Typescript generisk unionstyp


Jag försöker skapa en enkel switch-funktion som tar en första parameter som måste vara en union av sträng och ett objekt som har nycklar baserade på den första parameterunionen och kan returnera vilket värde som helst.

export const mySwitch = <T extends string>(value: T, possibilities: {[key in T]: any}): any => {
    return possibilities[value];
};

Typisk användning skulle vara

let option: "val1" | "val2" | "val3" = "val1";
// should returns s1
// Impossible should be type-checked as an error since it's not part of the option union type
mySwitch(option, {val1: "s1", val2: "s2", val3: "s3", impossible: "impossible"});

Mitt problem uppstår eftersom den generiska typen T måste vara en string för att kunna användas som ett objekt-nyckel. Jag vet inte hur man kan säga åt T att vara en union av string.

Jag försökte T extends string utan framgång.

Svar #1

Versionen T extends string verkar fungera bra. Den tillåter inte impossible, men skulle du inte vilja förbjuda det eftersom om parametern aldrig kan ha det värdet skulle det alternativet vara värdelöst?

export const mySwitch = <T extends string>(value: T, possibilities: {[key in T]: any}): any => {
    return possibilities[value];
};


declare let option: "val1" | "val2" | "val3";
mySwitch(option, {val1: "s1", val2: "s2", val3: "s3", impossible: "impossible"}); 

spela

Om du vill tillåta de extra nycklarna kan du deklarera case-objektet separat (kringgå kontroller för överflödiga egenskaper och låta dig återanvända case-objektet)

declare let option: "val1" | "val2" | "val3";
const casses = {val1: "s1", val2: "s2", val3: "s3", impossible: "impossible"}
mySwitch(option, casses); 

spela

Eller så kan du ändra din typ lite så att den generiska typparametern är case-objektet, och värdet kommer då att typas som keyof T:

export const mySwitch = <T>(value: keyof T, possibilities: T): any => {
    return possibilities[value];
};


declare let option: "val1" | "val2" | "val3";
mySwitch(option, {val1: "s1", val2: "s2", val3: "s3", impossible: "impossible"}); 

spela

Ett bättre alternativ skulle också vara att bevara typen från case-objektet istället för att använda any:

export const mySwitch = <T, K extends keyof T>(value: K, possibilities: T): T[K] => {
    return possibilities[value];
};


declare let option: "val1" | "val2" | "val3";
mySwitch(option, {val1: 1, val2: "s2", val3: "s3", impossible: false});  // returns string | number

spela

Redigera:

För att bevara både korrekt returtyp och fel om det finns möjligheter som inte är närvarande i unionen kan du använda detta:

const mySwitch = <T extends Record<K, any>, K extends string>(value: K, possibilities: T & Record<Exclude<keyof T, K>, never>): any => {
    return possibilities[value];
};

let option: "val1" | "val2" | "val3" = (["val1", "val2", "val3"] as const)[Math.round(Math.random() * 2)]
mySwitch(option, {val1: "s1", val2: "s2", val3: "s3" });
mySwitch(option, {val1: "s1", val2: "s2", val3: "s3", impossible: "" }); //err on impossible

spela

Observera att eftersom TypeScript gör kontrollflödesanalys måste du se till att option inte bara är typad som den faktiska konstanten du tilldelar istället för typannoteringen du anger

Relaterade artiklar