Hvordan bygge en type fra enum-verdier i TypeScript?


Gitt følgende:

enum FooKeys {
  FOO = 'foo',
  BAR = 'bar',
}

Jeg vil lage et grensesnitt som dette, men i stedet for å definere nøkler manuelt, bygge det ut fra enum-verdiene.

interface Foo {
  foo: string
  bar: string
}

Er noe slikt mulig med TypeScript?

Takk!

Svar #1

Hvordan bygge en type fra enum-verdier i TypeScript?

En enum kan inneholde streng- og tallverdier.

For strenger: du kan lage en strengunion fra enum-verdier ved å bruke en malstreng

enum FooKeys {
  FOO = 'foo',
  BAR = 'bar',
}

type FooValues =`${FooKeys}`; // this equals 'foo' | 'bar'

For tall: vi kan enten lage strengunion på samme måte eller: fra og med TS 5.0

TypeScript 5.0 klarer å gjøre alle enums om til union-enums ved å lage en unik type for hvert beregnet medlem. Det betyr at alle enums nå kan snevres inn og få medlemmene sine referert til som typer også.

Som betyr:

enum MagicNumbers {
a = 1,
b = 42
}

const numberA : MagicNumbers = 1;
const numberB : MagicNumbers = 2; // will raise an error since TS 5.0

Eksperimenter selv

For begge: Ved å kombinere det ovenfor kan vi bygge en EnumAsUnion-typehjelper som følger

enum-som-union.ts

type StringValues<T> = {
  [K in keyof T]: T[K] extends string ? T[K] : never;
}[keyof T];

type NumberValues<T> = {
  [K in keyof T]: T[K] extends number ? T[K] : never;
}[keyof T];

/**
 * Usage : type EnumValues = EnumAsUnion<typeof anEnum>
 */
type EnumAsUnion<T> = `${StringValues<T>}` | NumberValues<T>;

Eksempel :

import { EnumAsUnion } from 'enum-as-union';

enum anEnum {
  val1 = 'a',
  val2 = 'b',
  val3 = 1,
  val4 = 2,
}

type EnumValues = EnumAsUnion<typeof anEnum>;

let t: EnumValues;
t = 'a';
t = 'b';
t = 'c'; // error, as expected

t = 1;
t = 2;
t = 3; // error, as expected

Prøv det her

Svar #2

Ja, du kan bruke enum-verdier som nøkler. Og du kan bruke en mapped type som standardbibliotekets Record<K, V> for å forhindre gjentakelse:

enum FooKeys {
  FOO = 'foo',
  BAR = 'bar',
}

// probably all you need, but it's a type alias
type FooType = Record<FooKeys, string>;

// if you need an interface instead you can do this
interface FooInterface extends FooType {};

Og du kan verifisere at det fungerer:

declare const foo: FooInterface;
foo.foo; // okay
foo[FooKeys.FOO]; // okay

foo.bar; // okay
foo[FooKeys.BAR]; // okay

foo.baz; // error

Fungerer det for deg? Lykke til!

Svar #3

[@hackape sin løsning][1] er flott, men jeg fant minimal duplisering ved å utvide hans løsning som nedenfor:

type ReverseMap<T extends Record<keyof T, any>> = {
  [V in T[keyof T]]: {
    [K in keyof T]: T[K] extends V ? K : never;
  }[keyof T];
}

const Map = {
  'FOO': "foo" as "foo",
  'BAR': "bar" as "bar",
}

const reverseMap: ReverseMap<typeof Map> = Object.entries(Map).reduce((rMap, [k, v]) => {
  rMap[v] = k;
  return rMap;
}, {} as any);

export type Values = keyof typeof reverseMap; // 'foo' | 'bar';

Implementeringen av ReverseMap er godt forklart [her][2]

[1]: https://stackoverflow.com/a/60768453/5519365 [2]: https://stackoverflow.com/a/55209457/5519365

Oppdatering: Jeg fant en mye enklere løsning for ReverseMap

const Obj = {
 FOO: 'foo',
 BAR: 'bar',
} as const;

type ReverseMap<T> = T[keyof T];

export type Values = ReverseMap<typeof Obj>; // 'foo' | 'bar';

Relaterte artikler