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')
};
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.