typescript - Finns det ett sätt att begränsa en funktions parameter typ baserat på andra parametrar?


type ResourceA = { type: 'A', value: 'A1' | 'A2' | 'A3' };
type ResourceB = { type: 'B', value: 'B1' | 'B2' };
type Resource = ResourceA | ResourceB;

function singleParam(resource: Resource) {
  // ...
}

function multiParams<R extends Resource, T extends R['type'], V extends R['value']>(type:T, value: V) {
  // ... type and value should be 
}

// Examples
singleParam({ type: 'A', value: 'A1' });
singleParam({ type: 'A', value: 'B1' }); // type error

multiParams('A', 'A1');
multiParams('A', 'B1'); // no type error

Länk till Typescript Playground

Finns det något sätt att få detta sista exempel att orsaka ett typfel, precis som i det andra exemplet, men endast med två parametrar (type och value)?

Jag försökte denna lösning, men det skulle kräva att jag lägger till en tredje parameter som resource: R till multiParams-funktionen. Hur som helst, i det fallet skulle den första funktionen singleParam vara enklare.

Svar #1

Jag kan komma på två tillvägagångssätt.


Det första är att få multiParams() att acceptera en restparameter av en förening av tuplet-typer som vi omedelbart kan destrukturera till korrelerade variabler type och value. Det kan se ut så här:

type MultiParamsArgs = Resource extends infer R ? R extends Resource ?
  [type: R['type'], value: R['value']]
  : never : never;

/* type MultiParamsArgs = 
     [type: "A", value: "A1" | "A2" | "A3"] | 
     [type: "B", value: "B1" | "B2"];
*/

function multiParams(...[type, value]: MultiParamsArgs) { }

Här använder jag en distributiv villkors-typ över unionen i Resource, för att producera typen MultiParamsArgs, som du kan se är unionen av accepterade typer för argumentlistan i multiParams().

Och faktiskt, anrop till multiParams() kommer att acceptera korrekta parametrar och avvisa felaktiga:

multiParams('A', 'A1');
multiParams('A', 'B1'); // error
multiParams(Math.random() < 0.999 ? "A" : "B", "B1") // error here too

En fördel med detta tillvägagångssätt är att typkontrollören kommer att tolka type/value-paret av variabler som delar av en diskriminerad union, och du kan göra avgränsning på dem därefter:

function multiParams(...[type, value]: MultiParamsArgs) { 
  if (type === "A") {
    const x = { A1: 1, A2: 2, A3: 3 }[value] // okay
  }
}

En nackdel är att IntelliSense-stödet inte är så bra; på ett sätt fungerar en union av resttupler som en överbelastad funktion, men ibland visas bara den "första" överbelastningen i din editor. Det finns ett öppet ärende på microsoft/TypeScript#31977 som ber om förbättring där, men för tillfället är detta en begränsning.


Det andra tillvägagångssättet är att göra multiParams() generisk i nyckeltypen K av en hjälpmapptyp MultiParamsMap, så att vi kan representera kopplingen mellan typen K av type och den indexerade åtkomsttypen MultiParamsMap[K] av value. Det kan se ut så här:

type MultiParamsMap = { [T in Resource as T['type']]: T['value'] }
/* type MultiParams = {
    A: "A1" | "A2" | "A3";
    B: "B1" | "B2";
} */

function multiParams<K extends keyof MultiParamsMap>(
  type: K, value: MultiParamsMap[K]) { }

Så MultiParamsMap är en mappad typ med ommappade nycklar över Resource. För de flesta normala anrop av multiParams() accepterar detta korrekta argument och avvisar felaktiga.

multiParams('A', 'A1');
multiParams('A', 'B1'); // error

Och IntelliSense är definitivt användbart; så snart du skriver multiParams('A', härleder kompilatorn "A" för K och uppmanar dig sedan att ange "A1" | "A2" | "A3" för den andra parametern. Så det är en fördel.

Nackdelen här är att typkontrollanten inte kan förstå att kontrollen av type har någon effekt på value, så du kan inte använda dem som en diskriminerad union:

function multiParams<K extends keyof MultiParamsMap>(
  type: K, value: MultiParamsMap[K]) {

  if (type === "A") {
    const x = { A1: 1, A2: 2, A3: 3 }[value] // error!
  }

}

Och faktiskt är det tekniskt möjligt att skicka in ett value som inte motsvarar type på det sätt du vill:

multiParams(Math.random() < 0.999 ? "A" : "B", "B1") // okay?!

Det accepteras eftersom K antas vara unionen "A" | "B", och därför är MultiParamsMap[K] hela unionen "A1" | "A2" | "A3" | "B1" | "B2". Och därför accepteras "B1" som "value" även om det finns 99,9% chans att type är "A". Den här typen av situationer händer inte ofta i praktiken, så du behöver antagligen inte oroa dig för det från anroparens sida, men denna möjlighet är en anledning till varför implementeringssammansmala inte sker. Det finns öppna frågor om detta på microsoft/TypeScript#27808 och microsoft/TypeScript#33014, men för tillfället är detta en begränsning.


Om IntelliSense är den viktigaste aspekten för dig, då vill du förmodligen ha lösningen med generisk funktion.

Lekplatslänk till kod

Relaterade artiklar