Reputation: 1226
I am using TypeScript to define a constructor for a model in Angular. One of the properties in the model is set as an enum with a few possible string values. This works fine if I pass an enum value to the constructor. The problem is that I need call the constructor based on an API response which returns a string for the value that needs to be mapped to that property.
Is there any way of passing a string (as long as it is one of the values defined in the enum) to the constructor?
Enum:
export enum TestTypes {
FIRST = 'first',
SECOND = 'second',
}
export class Test {
this.myType: TestTypes;
constructor(options: {type: TestTypes}) {
this.myType = options.type;
}
}
The following works
const a = new Test({type:TestTypes.FIRST});
What I would like to achieve:
const b = new Test({type:'first'})
Should I do the following?
const b = new Test({type:TestTypes['first']})
Upvotes: 3
Views: 2322
Reputation: 329013
The easiest way to go is to change your enum
to a straight dictionary like this:
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]
The literal()
function is a helper that prompts the compiler to interpret the value as a string literal instead of widening to string
.
Now, the value TestTypes.FIRST
is exactly the string "first"
, the value TestTypes.SECOND
is exactly the string "second"
, and the type TestTypes
is exactly the union "first"|"second"
. That lets your class work as desired:
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
If you want to keep TestTypes
as an enum
, you can get what you want, but it's too much hoop jumping in my opinion.
First, if you wanted a standalone function that accepted either the enum
or the right string
values, you could make a generic function like this:
declare function acceptTestTypesOrString<E extends string>(
k: E & (Extract<TestTypes, E> extends never ? never : E)
): void;
I don't know if I should explain that, but it takes advantage of the fact that, say, TestTypes.FIRST extends "first"
. Let's see if it works:
acceptTestTypesOrString(TestTypes.FIRST) // okay
acceptTestTypesOrString(TestTypes.SECOND) // okay
acceptTestTypesOrString("first") // okay
acceptTestTypesOrString("second") // okay
acceptTestTypesOrString("third") // error
Looks good. But you want this as a constructor function. And you can't make a constructor function generic. Instead you could make the whole class generic, like this:
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
In this case, a
will be of type Test<TestTypes.FIRST>
and b
will be of type Test<"first">
. They are mostly interchangeable, but it seems suboptimal to drag around a generic type for the whole class when you only want it for the constructor.
But it works.
Okay, hope one of those ideas help. Good luck!
Upvotes: 3