¿Tipo genérico para obtener claves de enumeración como cadena de unión en texto mecanografiado?
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?
No, el consumidor deberá utilizar typeof MyEnum
para referirse al objeto cuyas claves son A
, B
y 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 null
en el siguiente código:
let maybeString: string | null = null;
La primera instancia de null
es 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 Foo
es un valor con nombre y el segundo Foo
es un tipo con nombre. Tenga en cuenta que el tipo del valor Foo
es {a: number}
, mientras que el tipo Foo
es {b: string}
. Ellos no son los mismos.
Incluso el typeof
operador lleva una doble vida. La expresión typeof x
siempre espera x
ser un valor , pero typeof x
en 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 Foo
arriba, el tipo del valor con nombre no es el tipo con nombre . Los grandes son class
y 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 Class
no 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 A
y B
, y cuyas propiedades son los elementos de la enumeración. Y typeof Enum
no 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 Enum
no 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 | 1
que 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 someKey
será "A" | "B"
. Sí, pero para usarlo como tipo tendrías que consultarlo:
type KeysOfEnum = typeof someKey;
lo que te obliga a usarlo typeof
nuevamente 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
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
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
!
Puedes simplemente pasar a type
en lugar de a value
y el compilador no se quejará. Esto lo logras typeof
como señalaste.
Será un poco menos automático:
type AnyEnumKeysAsStrings<TEnumType> = keyof TEnumType;
Que puedes utilizar como:
type MyEnumKeysAsStrings = AnyEnumKeysAsStrings<typeof MyEnum>;