Firstly, a type is created that associates enum keys with types:
enum MyEnum {
A,
B
}
type TypeMap = {
[MyEnum.A]:string,
[MyEnum.B]:number
}
interface ObjInterface<T extends keyof TypeMap> {
obj: T,
objData: TypeMap[T]
}
interface SecondaryInterface {
value: string,
objChosen: ObjInterface<keyof TypeMap>
}
Then an object is made where objData's type gets verified against the TypeMap:
myObj:SecondaryInterface = {value:"", objChosen:{obj:MyEnum.A, objData:"a string"}}
This partly works but when I type objData, it gives a union type hint 'string | number' and not just 'string' because it infers the type from keyof TypeMap rather than the exact TypeMap[T].
Is it possible to get an exact type match for the enum key used and its associated type set in the type map?
It can be made to work using a type assertion but can this be made to work without one?:
myObj:SecondaryInterface = {value:"", objChosen:<ObjInterface<MyEnum.A>>{obj:MyEnum.A, objData:"a string"}}
According to your definition, ObjInterface<MyEnum.A | MyEnum.B>
is a single object type where obj
can be any MyEnum
and objData
can be string | number
. But that's no what you want. You'd like ObjInterface<T>
to distribute over unions in T
, so that ObjInterface<MyEnum.A | MyEnum.B>
is evaluated as ObjInterface<MyEnum.A> | ObjInterface<MyEnum.B>
. There are different ways to accomplish that, such as using distributive conditional types, but when the thing you want to distribute over is keylike, I usually prefer to write a distributive object type as coined in microsoft/TypeScript#47109. That's where you index into a mapped type.
If you have a keylike type K
, and you want to distribute the type function F<K>
over unions in K
, then you can write {[P in K]: F<P>}[K]
. That turns into a mapped type with one property for each member of K
, which is immediately indexed into with K
to make a union of those properties.
For your code that looks like:
type ObjInterface<K extends keyof TypeMap> = {
[P in K]: {
obj: P,
objData: TypeMap[P]
}
}[K]
And then ObjInterface<keyof TypeMap>
evaluates to
/* type ObjInterface<keyof TypeMap> = {
obj: MyEnum.A;
objData: string;
} | {
obj: MyEnum.B;
objData: number;
} */
This isn't strictly an interface
anymore, so maybe the name should be changed.
Of course at this point you haven't actually shown a reason why you need ObjInterface
to continue to be generic, if all you care about is plugging in keyof TypeMap
there. If you don't need that the generic, you can hardcode K
to be keyof TypeMap
and make it
type ObjInterface = { [K in keyof TypeMap]: {
obj: K,
objData: TypeMap[K]
} }[keyof TypeMap]
And then the rest of your code is
interface SecondaryInterface {
value: string,
objChosen: ObjInterface
}
const myObj: SecondaryInterface =
{ value: "", objChosen: { obj: MyEnum.A, objData: "a string" } }; // okay
myObj.objChosen = { obj: MyEnum.B, objData: 123 }; // okay
myObj.objChosen = { obj: MyEnum.A, objData: 123 }; // error
as desired.