-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Type guards donβt always narrow generic types #44446
Comments
As you saw, type-guarding generics is done via intersections. Negating the result of an |
My proposal in the βMore general solutionβ is to use |
I'm thinking this is the same issue - if it's not, will create a new issue:
I can make TS happy in a few ways:
|
Not sure if this is the same, but there is something similar: class Gen<T extends string | undefined = string | undefined> {
isUndefined(): this is Gen<undefined> { ... }
}
const g = new Gen();
if (g.isUndefined())
throw new Error(); // correctly narrowed to Gen<undefined>
// but undefined isn't excluded here |
Bug Report
π Search Terms
generics, narrowing, type guards
π Version & Regression Information
β― Playground Link
This is nearly exactly how I discovered this bug.
π» Code
π Actual behavior
Thereβs an error on the word
listeners
on the line:The error differs between target versions.
Targeting ES2015 or higher:
Targeting ES5 or lower:
In context, the generic type
EventListeners<EventMap>[EventType]
is equivalent toArray<(event: EventMap[EventType]) => void> | undefined
.Since
undefined
is not an array type, TypeScript is right to complain⦠except that I already checked thatlisteners !== undefined
.π Expected behavior
TypeScript should narrow
EventListeners<EventMap>[EventType]
toArray<(event: EventMap[EventType]) => void>
, as it does when I calllisteners.forEach(β¦)
.Other findings
After some investigation, Iβve discovered the following.
Type narrowing with intersection types
With a type guard with return type
obj is GuardedType
, type narrowing for generics works correctly because TypeScript narrows the generic typeT
to an intersection typeT & GuardedType
.For instance:
However, if I use a type guard with return type
obj is undefined
and negate the result, then I see the behavior described in the issue.So, Iβm guessing this means that this kind of type narrowing only works for βpositiveβ type guards, i.e. those where the narrowed type can be an intersection type.
Type narrowing by constraint position
But then, why does
listeners.forEach(β¦)
work in both cases? Digging into the TypeScript source code and using a debugger, I found that the type oflisteners
differs between thelisteners.forEach(β¦)
call and thefor (const listener of listeners)
loop according to the following function:Because
isConstraintPosition(β¦)
is defined as:Therefore:
listeners
inlisteners.forEach(β¦)
:parent.kind === SyntaxKind.PropertyAccessExpression
isConstraintPosition(β¦) === true
substituteConstraints === true
getNarrowableTypeForReference(β¦)
returns the type((event: EventMap[EventType]) => void)[] | undefined
((event: EventMap[EventType]) => void)[]
according to control flowlisteners
infor (const listener of listeners)
:parent.kind === SyntaxKind.ForOfStatement
isConstraintPosition(β¦) === false
substituteConstraints === false
getNarrowableTypeForReference(β¦)
returns the typeEventListeners<EventMap>[EventType]
Straightforward solution
I believe the straightforward solution is to add a check for
parent.kind === SyntaxKind.ForOfStatement
in the definition ofisConstraintPosition(node: Node)
so that it would be defined as follows:However, this
isConstraintPosition(β¦)
check feels like a special case for just a few specific contexts. I can think of several examples that would cause such a check to fail:But patching it to include a check for
SyntaxKind.ForOfStatement
would be a quick and easy fix for my use case.More general solution
I found that if I manually cast
listeners
to typeExclude<typeof listeners, undefined>
, then this fixes my example:A similar kind of cast fixes the other example I presented showing failures of
isConstraintPosition(β¦)
:So, I feel like the general solution would be: when a type guard cannot narrow a generic type
T
to an intersection type, it should instead narrow the typeT
toExclude<T, β¦>
. (Is there a situation where this wouldnβt work?)Let me know what you all think, and thanks for creating my favorite programming language!
The text was updated successfully, but these errors were encountered: