Home > Net >  Narrowing generec type in typescript
Narrowing generec type in typescript

Time:10-21

typescript v4.4.4 Why generic type is not narrowed after type guard? How do I fix it?

type Data = A | B | C
type G<T extends Data> = {
  type: 'a' | 'b'
  data: T
}
type A = {
  foo: string
}
type B = {
  bar: string
}
type C = {
  foobar: string
}
const isA = (item: G<Data>): item is G<A> => item.type === 'a'

const throwOnA = (item: G<Data>): G<Exclude<Data, A>> => {
  if (!isA(item)) return item // Item still G<Data> instead of G<B | C>
  throw Error('is A')
}

CodePudding user response:

I think it's because Typescript cannot narrow G<Data> down that way. It only knows that item is of type G<Data> and not of type G<A>, but it cannot conclude that the resulting generic type should be G<B | C>. My best guess is that it's a limitation in its type resolver.

As a workaround you can create a function for the opposite check:

const isNotA = (item: G<Data>): item is G<Exclude<Data, A>> => item.type !== 'a' // or return !isA(item)

const throwOnA = (item: G<Data>): G<Exclude<Data, A>> => {
  if (isNotA(item)) return item // Item is now G<B | C>
  throw Error('is A')
}

I know this takes away some of the magic, but it's the best I can come up with.

CodePudding user response:

There are a couple of problems with your code. The main one is that G<Data> is not a union type, so narrowing is not going to work how you want. The other one (which isn't causing this issue, but it's part of the fix) is that your G type allows mismatched properties, e.g. {type: 'a', data: B}. The solution is to use a discriminated union type.

type GUnion =
  | {type: 'a', data: A}
  | {type: 'b', data: B | C} // or make B and C separate

type G<T extends Data> = Extract<GUnion, {data: T}>

const isA = (item: G<Data>): item is G<A> => item.type === 'a';

const throwOnA = (item: G<Data>): G<Exclude<Data, A>> => {
  if(!isA(item)) {
    return item;
  }
  throw Error('is A')
};

Playground Link

Note that you can also now write if(item.type !== 'a') instead of if(!isA(item)) to narrow the type, if you think that is clearer.

  • Related