Typescript-matchning mellan karta av funktionstyper och argumenttyper


Jag har ett objekt som kartlägger funktioner, och ett gränssnitt (med samma nycklar som objektet) som kartlägger argumenttyperna för dessa funktioner. Nu försöker jag skriva en funktion som kan anropa antingen det ena eller det andra, beroende på en nyckel. Här är en förenklad version av problemet:

interface OneFProps {
  a: string;
  b: string;
}

const oneF = (props: OneFProps) => {
  const { a, b } = props;
  return `oneF ${a} ${b}`;
};

interface TwoFProps {
  c: string;
  d: string;
}

const twoF = (props: TwoFProps) => {
  const { c, d } = props;
  return `oneF ${c} ${d}`;
};

const funcMap = {
  oneF: oneF,
  twoF: twoF,
};

interface typeMap {
  oneF: OneFProps;
  twoF: TwoFProps;
}

type tags = keyof typeMap;

type BlendedProps<T extends tags> = {
  key: T;
  z?: string;
  x?: string;
} & typeMap[T];

const blended = <T extends tags>(props: BlendedProps<T>) => {
  const { key, z, x, ...rest } = props;
  const func = funcMap[key];

  return func(rest as any);
};

console.log(blended<'oneF'>({ key: 'oneF', a: 'AAA', b: 'bbb' }));
console.log(blended<'twoF'>({ key: 'twoF', c: 'ccc', d: 'DDD' }));

Jag var tvungen att sätta det rest as any eftersom jag inte kunde få typerna att fungera. Jag gjorde lite research och hittade saker om distribuerade villkorliga typer, men jag vet inte om det verkligen är problemet här och kunde inte komma på en lösning för det. Finns det ett sätt att bevara typ-säkerheten här eller är mitt tillvägagångssätt konceptuellt fel?

Svar #1

Det underliggande problemet här har att göra med att kompilatorn inte kan se korrelationen mellan typen av func och typen av rest inne i implementeringen av blended(). Denna situation är i huvudsak ämnet för microsoft/TypeScript#30581, och den rekommenderade metoden för att hantera det beskrivs i microsoft/TypeScript#47109.

Om du vill att kompilatorn ska verifiera att funcMap[key] accepterar en parameter av typen rest, då behöver du uttrycka typen av funcMap explicit som en mappad typ över Tags-unionen, där varje egenskap tar ett enda argument av den typ som kompilatorn härleder för rest. Om du tittar inuti blended, är typen av rest Omit<BlendedProps<T>, "key" | "z" | "x">. Så du kan annotera funcMap så här:

const funcMap: { [T in Tags]: 
  (props: Omit<BlendedProps<T>, "key" | "z" | "x">) => string 
} = {
    oneF: oneF,
    twoF: twoF,
};

Detta förändrar egentligen inte typen av funcMap; faktiskt kan kompilatorn se att initialiseringsvärdet av typen { oneF: (props: OneFProps) => string; twoF: (props: TwoFProps) => string; } kan tilldelas variabeln av typen { [T in Tags]: (props: Omit<BlendedProps<T>, "key" | "z" | "x">) => string }, så de är kompatibla typer.

Men skillnaden i representation är viktig, eftersom nu implementeringen av blended() typkontroller:

const blended = <T extends Tags>(props: BlendedProps<T>) => {
    const { key, z, x, ...rest } = props;
    const func = funcMap[key];
    return func(rest); // okay
};

Faktiskt, nu är typen av funcMap[key] härledd till att vara (props: Omit<BlendedProps<T>, "key" | "z" | "x">) => string, och den parametertypen är identisk med typen av rest, så funcMap[key](rest) accepteras, som önskat.

Playground-länk till kod

Svar #2

Hoppas det hjälper dig

class Props<K extends string, P extends {}> {
  key: K;
  data: P;

  constructor(data: { key: K } & P) {
    this.key = data.key;
    this.data = data;
  }

  toString() {
    return JSON.stringify(this.data);
  }
}

const oneF = new Props({ key: 'oneF', a: 'AAA', b: 'bbb' });
console.log(oneF.toString());
// {"key":"oneF","a":"AAA","b":"bbb"}

const twoF = new Props({ key: 'twoF', c: 'ccc', d: 'DDD' });
console.log(twoF.toString());
// {"key":"twoF","c":"ccc","d":"DDD"}