Typescript: När är en typ spridbar?


Jag har problem med att använda objektspreading i Typescript-funktioner med Type-variabler.

Är det överhuvudtaget möjligt (just nu)? Om inte, vilka är kortfattade alternativ?

Här är vad jag observerade med både Typescript v2.6 och v2.7-dev:
I funktionsdefinitionerna nedan kompilerar de ok utan problem men de err ger följande kompileringsfel:

TS2698: Spridningstyper kan endast skapas från objekttyper.

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" };
};

Svar #1

Om du tittar på den härledda typen i TypeScript Playground för otherMessages i err1, är det

const otherMessages: { [field in TFieldNames]?: IMessages; }[TFieldNames]

Och det börjar fungera om du lägger till en explicit typ för 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" };
};

Det verkar som att TypeScript på egen hand inte kan förenkla { [field in TFieldNames]?: IMessages; }[TFieldNames] till IMessages | undefined. Ser ut som en bugg.

Svar #2

För att komplettera Artems svar: detta gränssnitt är inte giltigt i Playground :

interface MessagesMap<TFieldNames extends string> {
  [field in TFieldNames]?: IMessages; // TsError: A computed property name must be of type 'string', 'number', 'symbol' or 'any'
}

Så, err1-funktionen borde inte kompileras från början.

Om du behöver ett objekt med strängindexerade egenskaper uppmuntrar denna TypeScript-begränsning oss att förenkla designen och att föredra att använda en enkel string-typ istället för en mer precis typ som använder generiska och keyof- eller in-operatörer:

interface FieldMessages {
  [field: string]: IMessages;
}

Eller använd ett Map<K extends string, IMessages>-objekt.