typescript - ¿Tipo genérico para obtener claves de enumeración como cadena de unión en mecanografía?
generics enums (1)
No, el consumidor deberá usar
typeof MyEnum
para referirse al objeto cuyas claves son
A
,
B
y
C
MUCHA EXPLICACIÓN A CONTINUACIÓN, ALGUNAS DE LAS QUE PROBABLEMENTE YA SABES
Como probablemente sepa, TypeScript agrega un sistema de tipo estático a JavaScript, y ese sistema de tipo 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 el momento del 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 espera un valor e interpreta la expresión que encuentra como un valor si es posible, y otros lugares donde el compilador espera un tipo e interpreta la expresión que encuentra como un tipo si es posible.
Al compilador no le importa o se confunde si es posible que una expresión se interprete como un valor y un tipo.
Es perfectamente feliz, por ejemplo, con los dos sabores 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 problema con
let Foo = {a: 0};
type Foo = {b: string};
donde el primer
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 tipo de operador lleva una doble vida.
La expresión
typeof x
siempre espera que
x
sea un valor
, pero
typeof x
sí podría ser un valor o tipo dependiendo del 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;
lo hará a través de JavaScript, y utilizará el
operador de tipo de JavaScript
en el 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í están algunas introducciones de tipos nombrados:
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 camino hasta su pregunta ahora. Quieres inventar un operador de tipo que funcione así:
type KeysOfEnum = EnumKeysAsStrings<Enum>; // "A" | "B"
donde coloca el
tipo
Enum
y obtiene las claves del
objeto
Enum
.
Pero como se ve arriba,
el tipo
Enum
no es el mismo que el objeto
Enum
.
Y desafortunadamente 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 para el tipo
0 | 1
0 | 1
que produciría el tipo
"A" | "B"
"A" | "B"
.
Para que funcione, deberías pasarle un tipo que sepa sobre 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"
que
es
posible ... si
type EnumKeysAsString<T> = keyof T
Así que estás atascado haciendo que el consumidor especifique
typeof Enum
.
¿Hay soluciones?
Bueno, tal vez podrías usar algo que haga que un 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"
"A" | "B"
.
Sí, pero luego para usarlo como
tipo
tendrías que consultarlo:
type KeysOfEnum = typeof someKey;
lo que te obliga a usar
typeof
nuevo y es incluso 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 BLAH BLAH;
- TODAVÍA NO ES POSIBLE;
- LO SIENTO.
Espero que tenga sentido. Buena suerte.
Considere la siguiente enumeración mecanografiada:
enum MyEnum { A, B, C };
Si quiero otro tipo que sea las cadenas unidas de las claves de esa enumeración, 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 las enumeraciones de esta manera, para que pueda decir:
type MyEnumKeysAsStrings = AnyEnumKeysAsStrings<MyEnum>;
Me imagino la sintaxis correcta para eso 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 un valor".
Esto es inesperado y triste. De manera incompleta, puedo solucionarlo de la siguiente manera al eliminar el tipo de la parte derecha de la declaración del genérico y agregarlo 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, porque significa que el consumidor debe recordar hacer esta especificación espinosa de typeof en el genérico.
¿Hay alguna sintaxis que me permita especificar el tipo genérico como quiero inicialmente, para ser amable con el consumidor?