how can I create a type with the same keys as an enum, but each key has a different value, with this code :
export enum DataType {
TEST = "test",
OTHER = "other",
}
type ReturnTypes = {
[DataType.TEST]: number,
[DataType.OTHER]: string,
};
type DataTypeResolver<T extends DataType> = T | Lowercase<T>;
I get an error Type 'Lowercase<T>' cannot be used to index type 'ReturnTypes'.
when I try to use it in a method like this :
export async function getThing<T extends DataType>(dataType: DataTypeResolver<T>, text: string): Promise<ReturnTypes[Lowercase<T>] | null>
I want dataType
to be both uppercase and lowercase version of the key.
(Maybe I should stick to only Lowercase, but I'm not sure.)
CodePudding user response:
Enums are generally intended for situations where you only access the values via the enum keys; the actual values should be opaque to the TypeScript developer. They are one of the few places in TypeScript where types are treated nominally as opposed to structurally. In your DataType
enum, even though DataType.TEST
evaluates to "test"
at runtime, it is an error to assign "test"
to something that expects a value of the type DataType
:
let example = DataType.TEST;
example = "test"; // error! Type '"test"' is not assignable to type 'DataType'.
If that error is a source of frustration as opposed to a desirable feature, it's an indication that you don't really want enums. If you care about the particular values of an enum, it's an indication that you don't really want enums. Since you are relying on the fact that the values are lowercase versions of the keys, and since you want to be able to assign Lowercase<T>
to a place that expects DataType
, it seems that you don't really want enums.
In such situations you can often replace an enum with a const
-asserted object and get the desired behavior:
export const DataType = {
TEST: "test",
OTHER: "other",
} as const;
/* const DataType: {
readonly TEST: "test";
readonly OTHER: "other";
} */
The const
assertion tells the compiler to keep track of the literal types of the property values. So DataType
is known at compile time to be an object with a property with key "TEST"
and value "test"
and a property with key "OTHER"
and value "other"
.
Now it is fine to use the value "test"
in a place that expects a value of the same type as DataType.TEST
, because those are the same type:
let example = DataType.TEST;
example = "test"; // okay
Once you do that things become a lot easier. Your ReturnTypes
type can stay the same,
type ReturnTypes = {
[DataType.TEST]: number,
[DataType.OTHER]: string,
};
and now you can define the DataType
type as a union of the types of the keys and the types of the values of the DataType
object:
type DataType = keyof typeof DataType | typeof DataType[keyof typeof DataType];
// type DataType = "test" | "other" | "TEST" | "OTHER"
And the compiler will be happy letting you index into ReturnTypes
with Lowercase<DataType>
:
declare function getThing<K extends DataType>(
dataType: K, text: string): Promise<ReturnTypes[Lowercase<K>] | null> // okay
And you can see that getThing()
behaves as desired:
async function test() {
(await getThing("other", ""))?.toUpperCase(); // okay
(await getThing("TEST", ""))?.toFixed(); // okay
}