Narrowing call signatures doesn't work so well #10471
Labels
Design Limitation
Constraints of the existing architecture prevent this from being fixed
Suggestion
An idea for TypeScript
TL;DR Suggestion: when performing type narrowing, treat all call signatures as subtypes of
Function
, but unrelated to each other.First of all, I really appreciate all the great improvements to type analysis and narrowing that have gone into 2.0. A lot of plain code is now very accurately type-checked with no extra annotations or type assertions needed. Big thanks.
But! When it comes to narrowing call signatures, the nightly compiler rejects my runtime-valid code, and I have to engineer subtle workarounds (with a few
// don't change this!
comments) to keep it happy. I wonder whether an improvement in this area would be considered?Current Behaviour
Explanation: The compiler treats call signatures as all belonging to an implicit type hierarchy. In the example above,
Bar
is a subtype ofFoo
because it has fewer parameters. Due to subtype reduction, when the type guard narrowsfn
toFoo
, the narrowed type keepsBar
as well since it's a subtype ofFoo
. Conversely, in theelse
clause,fn
can't be aFoo
so it can't be aBar
either since that's a subtype ofFoo
.The Problem
isFoo
implementation).Foo
andBar
are clearly unrelated, looking at their declarations. Their relative arities don't imply a relationship. There is certainly no expectation that aBar
can be substituted wherever aFoo
is expected, which is what a subtype relationship generally implies (and what justifies the current narrowing behaviour).Current Workarounds
To remove compiler errors, the programmer must figure out if there are implicit type relationships between the call signatures they are trying to narrow. They can then either:
isBar
first would work perfectly. But this is not obvious or discoverable.Suggested Behaviour
When narrowing, treat all call signatures as subtypes of
Function
, but unrelated to each other. For example:typeof fn === 'function'
orfn instanceof Function
, then keep the current behaviour, since the intention is clearly to catch all call signatures regardless of arity or parameter/return types.Foo
, treatBar
as unrelated, so in theif
clausefn
isFoo
, and in theelse
clausefn
isBar
.Real-World Examples
Example 1: Express Middleware
Express middleware are functions, and there are two kinds: normal handlers and error handlers. Express tells them apart by arity: error handlers have four parameters, normal handlers have less than four (source code).
If we model this in TypeScript, we get the
Foo | Bar
situation above which doesn't compile. The suggested behaviour would fix this.Example 2: Subclassing Functions in ES6
GeneratorFunction
is a builtin subclass ofFunction
. In ES6, it's possible to effectively create our ownFunction
subclasses thanks toSymbol.hasInstance
. Here's a gist of how to do it in TypeScript.We can tell them apart using
instanceof
, for example:However whether narrowing works or gives compiler errors will depend on whether there exists an implicit subtype/supertype relationship between
MatchFunction
andNormalizeFunction
based on their call signatures, which is totally irrelevant. The suggested behaviour would fix this.The text was updated successfully, but these errors were encountered: