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!

Relaterte artikler