typescript - Crear un tipo con las mismas claves que un Enum pero cada clave tiene un valor diferente
cómo puedo crear un tipo con las mismas claves que un enum, pero cada clave tiene un valor diferente, con este código :
export enum DataType {
TEST = "test",
OTHER = "other",
}
type ReturnTypes = {
[DataType.TEST]: number,
[DataType.OTHER]: string,
};
type DataTypeResolver<T extends DataType> = T | Lowercase<T>;
Obtengo un error Type 'Lowercase<T>' cannot be used to index type 'ReturnTypes'. cuando intento usarlo en un método así :
export async function getThing<T extends DataType>(dataType: DataTypeResolver<T>, text: string): Promise<ReturnTypes[Lowercase<T>] | null>
Quiero que dataType sea tanto la versión en mayúsculas como en minúsculas de la clave. (Tal vez debería ceñirme solo a minúsculas, pero no estoy seguro.)
Respuesta #1
Los enums están generalmente destinados a situaciones donde solo se accede a los valores a través de las claves del enum; los valores reales deben ser opacos para el desarrollador de TypeScript. Son uno de los pocos lugares en TypeScript donde los tipos se tratan nominalmente en lugar de estructuralmente. En su enum DataType, aunque DataType.TEST se evalúa como "test" en tiempo de ejecución, es un error asignar "test" a algo que espera un valor del tipo DataType:
let example = DataType.TEST;
example = "test"; // error! Type '"test"' is not assignable to type 'DataType'.
Si ese error es una fuente de frustración en lugar de una característica deseable, es una indicación de que realmente no quieres enums. Si te importan los valores particulares de un enum, es una indicación de que realmente no quieres enums. Dado que estás confiando en el hecho de que los valores son versiones en minúscula de las claves, y dado que quieres poder asignar Lowercase<T> en un lugar que espera DataType, parece que realmente no quieres enums.
En tales situaciones, a menudo puedes reemplazar un enum con un objeto afirmado con const y obtener el comportamiento deseado:
export const DataType = {
TEST: "test",
OTHER: "other",
} as const;
/* const DataType: {
readonly TEST: "test";
readonly OTHER: "other";
} */
La afirmación const le indica al compilador que mantenga un seguimiento de los tipos literales de los valores de las propiedades. Así que DataType se conoce en tiempo de compilación como un objeto con una propiedad con la clave "TEST" y el valor "test" y una propiedad con la clave "OTHER" y el valor "other".
Ahora está bien usar el valor "test" en un lugar que espera un valor del mismo tipo que DataType.TEST, porque son del mismo tipo:
let example = DataType.TEST;
example = "test"; // okay
Una vez que hagas eso, las cosas se vuelven mucho más fáciles. Tu tipo ReturnTypes puede permanecer igual,
type ReturnTypes = {
[DataType.TEST]: number,
[DataType.OTHER]: string,
};
y ahora puedes definir el DataType tipo como una unión de los tipos de las claves y los tipos de los valores del objeto DataType:
type DataType = keyof typeof DataType | typeof DataType[keyof typeof DataType];
// type DataType = "test" | "other" | "TEST" | "OTHER"
Y el compilador estará contento permitiéndote indexar en ReturnTypes con Lowercase<DataType>:
declare function getThing<K extends DataType>(
dataType: K, text: string): Promise<ReturnTypes[Lowercase<K>] | null> // okay
Y puedes ver que getThing() se comporta como se desea:
async function test() {
(await getThing("other", ""))?.toUpperCase(); // okay
(await getThing("TEST", ""))?.toFixed(); // okay
}
Enlace de Playground al código
