¿Tipo genérico para obtener claves de enumeración como cadena de unión en texto mecanografiado?

Resuelto Stephan G asked hace 6 años • 5 respuestas

Considere la siguiente enumeración mecanografiada:

enum MyEnum { A, B, C };

Si quiero otro tipo que sean las cadenas unidas de las claves de ese enum, puedo hacer lo siguiente:

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

Esto es muy útil.

Ahora quiero crear un tipo genérico que funcione universalmente en enumeraciones de esta manera, de modo que pueda decir:

type MyEnumKeysAsStrings = AnyEnumKeysAsStrings<MyEnum>;

Me imagino que la sintaxis correcta sería:

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

Pero eso genera un error de compilación: "'TEnum' solo se refiere a un tipo, pero aquí se usa como valor".

Esto es inesperado y triste. Puedo solucionarlo de manera incompleta de la siguiente manera eliminando el tipo de del lado derecho de la declaración del genérico y agregándolo al parámetro de tipo en la declaración del tipo específico:

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

Sin embargo, no me gusta esta solución alternativa, porque significa que el consumidor debe recordar hacer esta complicada especificación de typeof en el genérico.

¿Existe alguna sintaxis que me permita especificar el tipo genérico como quiero inicialmente, para ser amable con el consumidor?

Stephan G avatar May 17 '18 00:05 Stephan G
Aceptado

No, el consumidor deberá utilizar typeof MyEnumpara referirse al objeto cuyas claves son A, By C.


LARGA EXPLICACIÓN POR DELANTE, ALGUNAS DE LAS CUALES PROBABLEMENTE YA CONOCES

Como probablemente sepa, TypeScript agrega un sistema de tipos estático a JavaScript, y ese sistema de tipos se borra cuando se transpila el código. La sintaxis de TypeScript es tal que algunas expresiones y declaraciones se refieren a valores que existen en tiempo de ejecución, mientras que otras expresiones y declaraciones se refieren a tipos que existen solo en tiempo de diseño/compilación. Los valores tienen tipos, pero no son tipos en sí mismos. Es importante destacar que hay algunos lugares en el código donde el compilador esperará un valor e interpretará la expresión que encuentre como un valor si es posible, y otros lugares donde el compilador esperará un tipo e interpretará la expresión que encuentre como un tipo si es posible.

Al compilador no le importa ni se confunde si es posible que una expresión se interprete como un valor y un tipo. Está perfectamente satisfecho, por ejemplo, con los dos tipos de nullen el siguiente código:

let maybeString: string | null = null;

La primera instancia de nulles un tipo y la segunda es un valor. Tampoco tiene ningún problema con

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

donde el primero Fooes un valor con nombre y el segundo Fooes un tipo con nombre. Tenga en cuenta que el tipo del valor Fooes {a: number}, mientras que el tipo Fooes {b: string}. Ellos no son los mismos.

Incluso el typeofoperador lleva una doble vida. La expresión typeof xsiempre espera xser un valor , pero typeof xen sí misma podría ser un valor o tipo según el contexto:

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

La línea let TypeofBar = typeof bar;llegará a JavaScript y utilizará el operador typeof de JavaScript en tiempo de ejecución y producirá una cadena. Pero type TypeofBar = typeof bar; se borra y utiliza el operador de consulta de tipo TypeScript para examinar el tipo estático que TypeScript ha asignado al valor denominado bar.


Ahora, la mayoría de las construcciones de lenguaje en TypeScript que introducen nombres crean un valor con nombre o un tipo con nombre. Aquí hay algunas introducciones de valores con nombre:

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

Y aquí hay algunas introducciones de tipos con nombre:

interface Type1 {}
type Type2 = string;

Pero hay algunas declaraciones que crean tanto un valor con nombre como un tipo con nombre y, como Fooarriba, el tipo del valor con nombre no es el tipo con nombre . Los grandes son classy enum:

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

Aquí, el tipo Class es el tipo de una instancia de Class, mientras que el valor Class es el objeto constructor . Y typeof Classno es 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;

Y el tipo Enum es el tipo de un elemento de la enumeración; una unión de los tipos de cada elemento. Mientras que el valor Enum es un objeto cuyas claves son Ay B, y cuyas propiedades son los elementos de la enumeración. Y typeof Enumno es 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}

Retrocediendo mucho hasta tu pregunta ahora. Quieres inventar un operador de tipo que funcione así:

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

donde pones el tipo Enum y sacas las claves del objeto Enum . Pero como ves arriba, el tipo Enumno es lo mismo que el objetoEnum . Y lamentablemente el tipo no sabe nada sobre el valor. Es algo así como decir esto:

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

Claramente, si lo escribe así, verá que no hay nada que pueda hacer con el tipo 0 | 1que produciría el tipo "A" | "B". Para que funcione, deberá pasarle un tipo que conozca el mapeo. Y ese tipo es typeof Enum...

type KeysOfEnum = EnumKeysAsStrings<typeof Enum>; 

que es como

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

lo cual es posible... si type EnumKeysAsString<T> = keyof T.


Así que estás estancado en hacer que el consumidor especifique typeof Enum. ¿Existen soluciones alternativas? Bueno, ¿quizás podrías usar algo que tenga ese valor, como una función?

 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)]; 
 }

Entonces puedes llamar

 const someKey = enumKeysAsString(Enum);

y el tipo de someKeyserá "A" | "B". Sí, pero para usarlo como tipo tendrías que consultarlo:

 type KeysOfEnum = typeof someKey;

lo que te obliga a usarlo typeofnuevamente y es aún más detallado que tu solución, especialmente porque no puedes hacer esto:

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

Blegh. Lo siento.


Recordar:

  • Esto no es posible
  • Tipos y valores bla, bla
  • Aún no es posible
  • Lo siento
jcalz avatar May 17 '2018 16:05 jcalz

Existe una solución que no requiere crear nuevos tipos genéricos.

Si declaras una enumeración

enum Season { Spring, Summer, Autumn, Winter };

Para llegar al tipo solo necesitas usar las palabras clave.keyof typeof

let seasonKey: keyof typeof Season;

Entonces la variable funciona como se esperaba.

seasonKey = "Autumn"; // is fine
// seasonKey = "AA" <= won't compile
Pascal Ganaye avatar Nov 06 '2021 12:11 Pascal Ganaye

En realidad es posible.

enum MyEnum { A, B, C };

type ObjectWithValuesOfEnumAsKeys = { [key in MyEnum]: string };

const a: ObjectWithValuesOfEnumAsKeys = {
    "0": "Hello",
    "1": "world",
    "2": "!",
};

const b: ObjectWithValuesOfEnumAsKeys = {
    [MyEnum.A]: "Hello",
    [MyEnum.B]: "world",
    [MyEnum.C]: "!",
};

// Property '2' is missing in type '{ 0: string; 1: string; }' but required in type 'ObjectWithValuesOfEnumAsKeys'.
const c: ObjectWithValuesOfEnumAsKeys = {  //  Invalid! - Error here!
    [MyEnum.A]: "Hello",
    [MyEnum.B]: "world",
};

// Object literal may only specify known properties, and '6' does not exist in type 'ObjectWithValuesOfEnumAsKeys'.
const d: ObjectWithValuesOfEnumAsKeys = {
    [MyEnum.A]: "Hello",
    [MyEnum.B]: "world",
    [MyEnum.C]: "!",
    6: "!",  //  Invalid! - Error here!
};

Enlace al patio de juegos


EDITAR: ¡Limitación eliminada!

enum MyEnum { A, B, C };

type enumValues = keyof typeof MyEnum;
type ObjectWithKeysOfEnumAsKeys = { [key in enumValues]: string };

const a: ObjectWithKeysOfEnumAsKeys = {
    A: "Hello",
    B: "world",
    C: "!",
};

// Property 'C' is missing in type '{ 0: string; 1: string; }' but required in type 'ObjectWithValuesOfEnumAsKeys'.
const c: ObjectWithKeysOfEnumAsKeys = {  //  Invalid! - Error here!
    A: "Hello",
    B: "world",
};

// Object literal may only specify known properties, and '6' does not exist in type 'ObjectWithValuesOfEnumAsKeys'.
const d: ObjectWithKeysOfEnumAsKeys = {
    A: "Hello",
    B: "world",
    C: "!",
    D: "!",  //  Invalid! - Error here!
};

Enlace al patio de juegos


  • ¡ Esto también funciona const enum!
Akxe avatar May 23 '2020 15:05 Akxe

Puedes simplemente pasar a typeen lugar de a valuey el compilador no se quejará. Esto lo logras typeofcomo señalaste.
Será un poco menos automático:

type AnyEnumKeysAsStrings<TEnumType> = keyof TEnumType;

Que puedes utilizar como:

type MyEnumKeysAsStrings = AnyEnumKeysAsStrings<typeof MyEnum>;
Nico Jones avatar Jun 01 '2021 08:06 Nico Jones