Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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" };
};
like image 387
Raffael Avatar asked Sep 11 '26 14:09

Raffael


1 Answers

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.

like image 150
artem Avatar answered Sep 14 '26 04:09

artem