Raffael
Raffael

Reputation: 2669

Typescript: When is a type spreadable?

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

Answers (2)

Romain Deneau
Romain Deneau

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

artem
artem

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

Related Questions