Analyser streng som Typescript Enum
Gitt en enum som ser slik ut:
export enum UsedProduct {
Yes = 'yes',
No = 'no',
Unknown = 'unknown',
}
Jeg vil gjerne skrive en funksjon som tar et sett med streng-litteraler og returnerer en forekomst av UsedProduct. Så langt har jeg skrevet en funksjon som dette:
export function parseUsedProduct(usedProdStr: 'yes' | 'no' | 'unknown'): UsedProduct {
switch (usedProdStr) {
case 'yes':
return UsedProduct.Yes;
case 'no':
return UsedProduct.No;
case 'unknown':
return UsedProduct.Unknown;
default:
return unknownUsedProductValue(usedProdStr);
}
}
function unknownUsedProductValue(usedProdStr: never): UsedProduct {
throw new Error(`Unhandled UsedProduct value found ${usedProdStr}`);
}
Denne implementeringen er ikke så bra fordi jeg må omdefinere de mulige verdiene i enumen. Hvordan kan jeg skrive om denne funksjonen slik at jeg ikke må definere 'yes' | 'no' | 'unknown'?
Svar #1
TS4.1 SVAR:
type UsedProductType = `${UsedProduct}`;
FORHÅNDS TS-4.1 SVAR:
TypeScript gjør ikke dette enkelt for deg, så svaret er ikke en én-liner.
En enum-verdi som UsedProduct.Yes er bare en streng- eller tall-literal ved kjøretid (i dette tilfellet strengen "yes"), men ved kompileringstid behandles den som en underkategori av streng- eller tall-literal. Så, UsedProduct.Yes extends "yes" er sant. Dessverre, gitt typen UsedProduct.Yes, finnes det ingen programmessig måte å utvide typen til "yes"... eller, gitt typen UsedProduct, finnes det ingen programmessig måte å utvide den til "yes" | "no" | "unknown". Språket mangler noen funksjoner som du vil trenge for å gjøre dette.
Det er en måte å lage en funksjonssignatur som oppfører seg som parseUsedProduct, men som bruker generiske typer og betingede typer for å oppnå dette:
type Not<T> = [T] extends [never] ? unknown : never
type Extractable<T, U> = Not<U extends any ? Not<T extends U ? unknown : never> : never>
declare function asEnum<E extends Record<keyof E, string | number>, K extends string | number>(
e: E, k: K & Extractable<E[keyof E], K>
): Extract<E[keyof E], K>
const yes = asEnum(UsedProduct, "yes"); // UsedProduct.yes
const no = asEnum(UsedProduct, "no"); // UsedProduct.no
const unknown = asEnum(UsedProduct, "unknown"); // UsedProduct.unknown
const yesOrNo = asEnum(UsedProduct,
Math.random()<0.5 ? "yes" : "no"); // UsedProduct.yes | UsedProduct.no
const unacceptable = asEnum(UsedProduct, "oops"); // error
I utgangspunktet tar det en enum-objekttype E og en streng-eller-tall-type K, og prøver å hente ut egenskapsverdien(e) til E som utvider K. Hvis ingen verdier av E utvider K (eller hvis K er en unionstype der en av delene ikke tilsvarer noen verdi av E), vil kompilatoren gi en feil. Spesifikasjonene for hvordan Not<> og Extractable<> fungerer, er tilgjengelige på forespørsel.
Når det gjelder implementeringen av funksjonen, vil du sannsynligvis trenge å bruke en typepåstand. Noe sånt som:
function asEnum<E extends Record<keyof E, string | number>, K extends string | number>(
e: E, k: K & Extractable<E[keyof E], K>
): Extract<E[keyof E], K> {
// runtime guard, shouldn't need it at compiler time
if (Object.values(e).indexOf(k) < 0)
throw new Error("Expected one of " + Object.values(e).join(", "));
return k as any; // assertion
}
Det burde fungere. I ditt spesifikke tilfelle kan vi hardkode UsedProduct:
type Not<T> = [T] extends [never] ? unknown : never
type Extractable<T, U> = Not<U extends any ? Not<T extends U ? unknown : never> : never>
function parseUsedProduct<K extends string | number>(
k: K & Extractable<UsedProduct, K>
): Extract<UsedProduct, K> {
if (Object.values(UsedProduct).indexOf(k) < 0)
throw new Error("Expected one of " + Object.values(UsedProduct).join(", "));
return k as any;
}
const yes = parseUsedProduct("yes"); // UsedProduct.yes
const unacceptable = parseUsedProduct("oops"); // error
Håper det hjelper. Lykke til!
