Alexander Farkas
Alexander Farkas

Reputation: 39

Picking a type from an interface map based on map key

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

Answers (1)

Linda Paiste
Linda Paiste

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.

  1. You can do what you have done which is broaden the return type to ABMap[keyof ABMap] aka A | B

  2. 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'};
  } 
}
  1. You can keep your generics and tell typescript that what you are returning is in fact the right type by using the 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];
  }
}

Playground Link

Upvotes: 1

Related Questions