web-dev-qa-db-fra.com

Type générique pour obtenir les clés d'énumération en tant que chaîne d'union dans un texte dactylographié?

Considérez l'énumération TypeScript suivante:

enum MyEnum { A, B, C };

Si je veux un autre type qui est les chaînes réunies des clés de cette énumération, je peux faire ce qui suit:

type MyEnumKeysAsStrings = keyof typeof MyEnum;  // "A" | "B" | "C"

C'est très utile.

Maintenant, je veux créer un type générique qui fonctionne universellement sur les énumérations de cette manière, afin que je puisse dire à la place:

type MyEnumKeysAsStrings = AnyEnumKeysAsStrings<MyEnum>;

J'imagine que la syntaxe correcte pour cela serait:

type AnyEnumKeysAsStrings<TEnum> = keyof typeof TEnum; // TS Error: 'TEnum' only refers to a type, but is being used as a value here.

Mais cela génère une erreur de compilation: "'TEnum' fait uniquement référence à un type, mais est utilisé ici comme valeur."

C'est inattendu et triste. Je peux contourner ce problème de la manière suivante en supprimant le typeof du côté droit de la déclaration du générique et en l'ajoutant au paramètre type dans la déclaration du type spécifique:

type AnyEnumAsUntypedKeys<TEnum> = keyof TEnum;
type MyEnumKeysAsStrings = AnyEnumAsUntypedKeys<typeof MyEnum>; // works, but not kind to consumer.  Ick.

Je n'aime pas cette solution de contournement, car cela signifie que le consommateur doit se rappeler de faire cette spécification icky de typeof sur le générique.

Y a-t-il une syntaxe qui me permettra de spécifier le type générique comme je le souhaite initialement, pour être gentil avec le consommateur?

15
Stephan G

Non, le consommateur devra utiliser typeof MyEnum pour faire référence à l'objet dont les clés sont A, B et C.


LONGUE EXPLICATION À VENIR, CERTAINS DONT VOUS SAVEZ DÉJÀ PROBABLEMENT

Comme vous le savez probablement, TypeScript ajoute un système de type statique à JavaScript, et ce système de type est effacé lorsque le code est transpilé. La syntaxe de TypeScript est telle que certaines expressions et instructions font référence à valeurs qui existent au moment de l'exécution, tandis que d'autres expressions et instructions font référence à types qui n'existent que lors de la conception/temps de compilation. Valeurs have types, mais ce ne sont pas des types eux-mêmes. Surtout, il existe des endroits dans le code où le compilateur attend une valeur et interprète l'expression qu'il trouve comme une valeur si possible, et d'autres endroits où le compilateur attend un type et interprète l'expression qu'il trouve comme un type si possible.

Le compilateur ne se soucie pas ou ne se confond pas s'il est possible qu'une expression soit interprétée à la fois comme une valeur et un type. Il est parfaitement satisfait, car par exemple, avec les deux versions de null dans le code suivant:

let maybeString: string | null = null;

La première instance de null est un type et la seconde est une valeur. Il n'a également aucun problème avec

let Foo = {a: 0};
type Foo = {b: string};   

où le premier Foo est une valeur nommée et le second Foo est un type nommé. Notez que le type de la valeur Foo est {a: number}, tandis que le type Foo est {b: string}. Ils ne sont pas les mêmes.

Même l'opérateur typeof mène une double vie. L'expression typeof x attend toujours que x soit une valeur, mais typeof x lui-même peut être une valeur ou un type selon le contexte:

let bar = {a: 0};
let TypeofBar = typeof bar; // the value "object"
type TypeofBar = typeof bar; // the type {a: number}

La ligne let TypeofBar = typeof bar; se rendra au JavaScript, et il utilisera opérateur typeof JavaScript lors de l'exécution et produira une chaîne. Mais type TypeofBar = typeof bar; est effacé et utilise opérateur de requête de type TypeScript pour examiner le type statique que TypeScript a affecté à la valeur nommée bar.


Désormais, la plupart des constructions de langage dans TypeScript qui introduisent des noms créent soit une valeur nommée soit un type nommé. Voici quelques introductions de valeurs nommées:

const value1 = 1;
let value2 = 2;
var value3 = 3;
function value4() {}

Et voici quelques introductions de types nommés:

interface Type1 {}
type Type2 = string;

Mais il y a quelques déclarations qui créent à la fois une valeur nommée et a type nommé, et, comme Foo ci-dessus, le type de la valeur nommée n'est pas le type nommé . Les plus gros sont class et enum:

class Class { public prop = 0; }
enum Enum { A, B }

Ici, le typeClass est le type d'un instance de Class, tandis que le valeurClass est l'objet constructeur. Et typeof Class n'est pas Class:

const instance = new Class();  // value instance has type (Class)
// type (Class) is essentially the same as {prop: number};

const ctor = Class; // value ctor has type (typeof Class)
// type (typeof Class) is essentially the same as new() => Class;

Et, le typeEnum est le type d'un élément de l'énumération; une union des types de chaque élément. Alors que la valeurEnum est un objet dont les clés sont A et B, et dont les propriétés sont les éléments de l'énumération. Et typeof Enum n'est pas Enum:

const element = Math.random() < 0.5 ? Enum.A : Enum.B; 
// value element has type (Enum)
// type (Enum) is essentially the same as Enum.A | Enum.B
//  which is a subtype of (0 | 1)

const enumObject = Enum;
// value enumObject has type (typeof Enum)
// type (typeof Enum) is essentially the same as {A: Enum.A; B: Enum.B}
//  which is a subtype of {A:0, B:1}

Revenons maintenant à votre question. Vous voulez inventer un opérateur de type qui fonctionne comme ceci:

type KeysOfEnum = EnumKeysAsStrings<Enum>;  // "A" | "B"

où vous mettez le typeEnum dans, et obtenez les clés de objetEnum out. Mais comme vous le voyez ci-dessus, le type Enum n'est pas le même que l'objet Enum. Et malheureusement, le type ne sait rien de la valeur. C'est un peu comme dire ceci:

type KeysOfEnum = EnumKeysAsString<0 | 1>; // "A" | "B"

Clairement, si vous l'écrivez comme ça, vous verriez qu'il n'y a rien que vous puissiez faire pour le type 0 | 1 qui produirait le type "A" | "B". Pour le faire fonctionner, vous devez lui passer un type qui connaît le mappage. Et ce type est typeof Enum...

type KeysOfEnum = EnumKeysAsStrings<typeof Enum>; 

qui est comme

type KeysOfEnum = EnumKeysAsString<{A:0, B:1}>; // "A" | "B"

qui est possible ... si type EnumKeysAsString<T> = keyof T.


Vous êtes donc obligé de demander au consommateur de spécifier typeof Enum. Existe-t-il des solutions de contournement? Eh bien, vous pourriez peut-être utiliser quelque chose qui a cette valeur, comme une fonction?

 function enumKeysAsString<TEnum>(theEnum: TEnum): keyof TEnum {
   // eliminate numeric keys
   const keys = Object.keys(theEnum).filter(x => 
     (+x)+"" !== x) as (keyof TEnum)[];
   // return some random key
   return keys[Math.floor(Math.random()*keys.length)]; 
 }

Ensuite, vous pouvez appeler

 const someKey = enumKeysAsString(Enum);

et le type de someKey sera "A" | "B". Oui, mais pour l'utiliser comme type, il faudrait l'interroger:

 type KeysOfEnum = typeof someKey;

ce qui vous oblige à réutiliser typeof et est encore plus détaillé que votre solution, d'autant plus que vous ne pouvez pas faire ceci:

 type KeysOfEnum = typeof enumKeysAsString(Enum); // error

Blegh. Pardon.


RÉCAPITULER:

  • CECI IS PAS POSSIBLE;
  • TYPES ET VALEURS BLAH BLAH;
  • TOUJOURS PAS POSSIBLE;
  • PARDON.

J'espère que cela a du sens. Bonne chance.

35
jcalz