An efficient solution is to modify your enum
into a direct dictionary structure like the following:
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 acts as a helper that instructs the compiler to interpret the value as a string literal rather than expanding it to string
.
Now, TestTypes.FIRST
holds the exact value of the string "first"
, TestTypes.SECOND
contains the exact string "second"
, and the type TestTypes
represents the union "first"|"second"
. This configuration allows your class to perform as intended:
export class Test {
myType: TestTypes; // note that this is an annotation, not an initializer
constructor(options: { type: TestTypes }) {
// use options.type instead of just type
this.myType = options.type;
}
}
const a = new Test({ type: TestTypes.FIRST }); // runs smoothly
const b = new Test({ type: "first" }); // also works fine as they are equivalent
If you prefer keeping TestTypes
as an enum
, achieving your objective is feasible but involves complex steps.
To begin with, if you aim for a standalone function that accepts either the enum
or the correct string
values, you can create a generic function as shown below:
declare function acceptTestTypesOrString<E extends string>(
k: E & (Extract<TestTypes, E> extends never ? never : E)
): void;
This implementation leverages the fact that TestTypes.FIRST extends "first"
. Let's test its functionality:
acceptTestTypesOrString(TestTypes.FIRST) // valid
acceptTestTypesOrString(TestTypes.SECOND) // acceptable
acceptTestTypesOrString("first") // works fine
acceptTestTypesOrString("second") // no issues
acceptTestTypesOrString("third") // triggers an error
The results look promising. However, transforming this into a constructor function poses a challenge since constructor functions cannot be generic. In this scenario, making the entire class generic seems like a suitable alternative:
cclass Test<E extends string> {
myType: E; // remember, this is an annotation, not an initializer
constructor(options: {
type: E & (Extract<TestTypes, E> extends never ? never : E)
}) {
// assign options.type instead of type
this.myType = options.type;
}
}
const a = new Test({ type: TestTypes.FIRST }); // operational
const b = new Test({ type: "first" }); // effective as well
In this setup, a
will correspond to type Test<TestTypes.FIRST>
while b
will align with Test<"first">
. Although they are largely interchangeable, introducing a generic type for the entire class when it is needed solely for the constructor may seem suboptimal.
Nevertheless, this approach delivers the desired outcome.
Hopefully, one of these suggestions proves useful. Best of luck!