Reputation: 2669
I'm having trouble using object spread in Typescript functions with Type variables.
Is it possible at all (as of now)? If not, what are concise alternatives?
Here is what I observed with both Typescript v2.6 and v2.7-dev:
In the function definitions below, the ok
ones compile just fine but the err
ones give the following compiler error:
TS2698: Spread types may only be created from object types.
interface IMessages {
[msgKey: string]: string;
}
const ok1 = () => {
type TFieldNames = "a" | "b" | "c";
const fieldErrors: { [field in TFieldNames]?: IMessages } = {};
const fieldName = "XXX" as TFieldNames;
const otherMessages = fieldErrors[fieldName]; // has type IMessages
fieldErrors[fieldName] = { ...otherMessages, too_big: "is too big" };
};
const ok2 = () => {
type TFieldNames = keyof { a: number; b: number; c: number };
const fieldErrors: { [field in TFieldNames]?: IMessages } = {};
const fieldName = "XXX" as TFieldNames;
const otherMessages = fieldErrors[fieldName]; // has type IMessages
fieldErrors[fieldName] = { ...otherMessages, too_big: "is too big" };
};
const ok3 = () => {
type TFieldNames = string;
const fieldErrors: { [field in TFieldNames]?: IMessages } = {};
const fieldName = "XXX" as TFieldNames;
const otherMessages = fieldErrors[fieldName]; // has type "any"
fieldErrors[fieldName] = { ...otherMessages, too_big: "is too big" };
};
const err1 = <TFieldNames extends string>() => {
const fieldErrors: { [field in TFieldNames]?: IMessages } = {};
const fieldName = "XXX" as TFieldNames;
const otherMessages = fieldErrors[fieldName]; // has type "any"
fieldErrors[fieldName] = { ...otherMessages, too_big: "is too big" };
};
const err2 = <TFields extends { [key: string]: any }>() => {
const fieldErrors: { [field in keyof TFields]?: IMessages } = {};
const fieldName = "XXX" as keyof TFields;
const otherMessages = fieldErrors[fieldName]; // has type "any"
fieldErrors[fieldName] = { ...otherMessages, too_big: "is too big" };
};
const err3 = <TFields extends object>() => {
const fieldErrors: { [field in keyof TFields]?: IMessages } = {};
const fieldName = "XXX" as keyof TFields;
const otherMessages = fieldErrors[fieldName]; // has type "any"
fieldErrors[fieldName] = { ...otherMessages, too_big: "is too big" };
};
Upvotes: 3
Views: 1192
Reputation: 3061
To complement artem's answer: this interface is not a valid in the Playground :
interface MessagesMap<TFieldNames extends string> {
[field in TFieldNames]?: IMessages; // TsError: A computed property name must be of type 'string', 'number', 'symbol' or 'any'
}
So, err1
function should not compiled from the beginning.
If you need an object with string-indexed properties, this TypeScript constraint encourages us to simplify the design and to prefer using a simple string
type instead of a more precise type using generic and keyof
or in
operators:
interface FieldMessages {
[field: string]: IMessages;
}
Or use a Map<K extends string, IMessages>
object.
Upvotes: 0
Reputation: 51689
If you look at the inferred type in typescript playground for otherMessages
in err1
, it's
const otherMessages: { [field in TFieldNames]?: IMessages; }[TFieldNames]
And it starts working if you add explicit type for otherMessages
:
const err1b = <TFieldNames extends string>() => {
const fieldErrors: { [field in TFieldNames]?: IMessages } = {};
const fieldName = "XXX" as TFieldNames;
const otherMessages: IMessages | undefined = fieldErrors[fieldName];
fieldErrors[fieldName] = { ...otherMessages, too_big: "is too big" };
};
It seems that typescript on its own is unable to simplify { [field in TFieldNames]?: IMessages; }[TFieldNames]
to IMessages | undefined
. Looks like a bug.
Upvotes: 1