angular - Constructor de Typescript: aceptar cadena para enum
Estoy usando TypeScript para definir un constructor para un modelo en Angular. Una de las propiedades del modelo está definida como un enum con algunos valores posibles de tipo string. Esto funciona bien si paso un valor del enum al constructor. El problema es que necesito llamar al constructor basado en una respuesta de la API que devuelve un string para el valor que debe ser asignado a esa propiedad.
¿Hay alguna manera de pasar una cadena (siempre que sea uno de los valores definidos en el enum) al constructor?
Enumeración :
export enum TestTypes {
FIRST = 'first',
SECOND = 'second',
}
export class Test {
this.myType: TestTypes;
constructor(options: {type: TestTypes}) {
this.myType = options.type;
}
}
Los siguientes trabajos const a=new Test({type:TestTypes.FIRST});
Lo que me gustaría lograr: const b=new Test({type:'first'})
¿Debería hacer lo siguiente? const b=new Test({type:TestTypes['first']})
Respuesta #1
La forma más fácil de hacerlo es cambiar tu enum a un diccionario directo como este:
const literal = <L extends string | number | boolean>(l: L) => l;
export const TestTypes = {
FIRST: literal('first'),
SECOND: literal('second'),
};
export type TestTypes = (typeof TestTypes)[keyof typeof TestTypes]
La función literal() es un ayudante que solicita al compilador que interprete el valor como un literal de cadena en lugar de ampliarlo a string.
Ahora, el valor TestTypes.FIRST es exactamente la cadena "first", el valor TestTypes.SECOND es exactamente la cadena "second", y el tipo TestTypes es exactamente la unión "first"|"second". Esto permite que tu clase funcione como se desea:
export class Test {
myType: TestTypes; // this is an annotation, not an initializer, right?
constructor(options: { type: TestTypes }) {
// options.type, not type
this.myType = options.type;
}
}
const a = new Test({ type: TestTypes.FIRST }); // okay
const b = new Test({ type: "first" }); // okay... it's the same thing
Si quieres mantener TestTypes como un enum, puedes obtener lo que quieres, pero en mi opinión es demasiado complicarse la vida.
Primero, si querías una función independiente que aceptara ya sea el enum o los valores correctos de string, podrías hacer una función genérica como esta:
declare function acceptTestTypesOrString<E extends string>(
k: E & (Extract<TestTypes, E> extends never ? never : E)
): void;
No sé si debería explicar eso, pero se aprovecha del hecho de que, digamos, TestTypes.FIRST extends "first". Veamos si funciona:
acceptTestTypesOrString(TestTypes.FIRST) // okay
acceptTestTypesOrString(TestTypes.SECOND) // okay
acceptTestTypesOrString("first") // okay
acceptTestTypesOrString("second") // okay
acceptTestTypesOrString("third") // error
Se ve bien. Pero quieres esto como una función constructora. Y no puedes hacer que una función constructora sea genérica. En su lugar, podrías hacer que toda la clase sea genérica, así:
export class Test<E extends string> {
myType: E; // this is an annotation, not an initializer, right?
constructor(options: {
type: E & (Extract<TestTypes, E> extends never ? never : E)
}) {
// options.type, not type
this.myType = options.type;
}
}
const a = new Test({ type: TestTypes.FIRST }); // okay
const b = new Test({ type: "first" }); // also okay
En este caso, a será de tipo Test<TestTypes.FIRST> y b será de tipo Test<"first">. Son mayormente intercambiables, pero parece subóptimo arrastrar un tipo genérico para toda la clase cuando solo lo quieres para el constructor.
Pero funciona.
Está bien, espero que una de esas ideas ayude. ¡Buena suerte!
