Reputation: 39
I want to code a general create method that produces different objects that are typed inside of a map. The problem is that typescript mixes all interfaces together instead of only selecting one in the interface map:
interface A {
a: string;
}
interface B {
b: string;
}
interface ABMap {
a: A;
b: B;
}
function create<ID extends keyof ABMap>(id: ID): ABMap[ID] { // this is now combined A & B instead of A | B
if (id === 'a') {
return {a: 'a'}; // error a is missing b key
} else if (id === 'b') {
return {b: 'b'};
}
}
Upvotes: 1
Views: 244
Reputation: 42228
There are two issues here.
First, typescript does not narrow the definition of the generic based on type guards. This is a known and expected behavior, although it can be frustrating. When you check if (id === 'a')
, typescript knows that the value of the variable id
is of type A
. However it does not narrow the generic to just A
. It is perfectly valid for the generic type ID
to be the union A | B
and the variable id
to be A
. So when that check is passed, we know that ID
must include A
, but we don't know that it is "A
and only A
".
The second issue is why the type ABMap[ID]
is getting narrowed to A & B
instead of A | B
. That one I cannot explain.
There are (at least) three solutions.
You can do what you have done which is broaden the return type to ABMap[keyof ABMap]
aka A | B
If you are dealing with a small set of pairings, like the two in this example, you can associate the inputs and outputs through function overloads:
function create(id: 'a'): ABMap['a']
function create(id: 'b'): ABMap['b']
function create(id: keyof ABMap): ABMap[keyof ABMap] {
if (id === 'a') {
return {a: 'a'};
} else {
return {b: 'b'};
}
}
as
keyword.function create<ID extends keyof ABMap>(id: ID): ABMap[ID] {
if (id === 'a') {
return {a: 'a'} as ABMap[ID];
} else {
return {b: 'b'} as ABMap[ID];
}
}
Upvotes: 1