Description
π Search Terms
- intersect
- conditional
- generic
- narrow
- typeof
- function
- callable
- ts2349
π Version & Regression Information
This is the behavior in every version I tried, and I reviewed the FAQ for entries about narrowing.
β― Playground Link
π» Code
type Thunk<A> = () => A;
type NonFunction<A> = A extends Function ? never : A;
type Expr<A> = Thunk<A> | NonFunction<A>;
const evaluate = <A>(expr: Expr<A>): A =>
typeof expr === "function" ? expr() : expr;
// ^^^^
// -------------------------------------------------------------------------------------
// This expression is not callable.
// Not all constituents of type 'Thunk<A> | (NonFunction<A> & Function)' are callable.
// Type 'NonFunction<A> & Function' has no call signatures. (2349)
// -------------------------------------------------------------------------------------
// (parameter) expr: Thunk<A> | (NonFunction<A> & Function)
// -------------------------------------------------------------------------------------
π Actual behavior
The parameter expr
, which has the type Expr<A>
, is being narrowed by the typeof expr === "function"
condition. When the condition is true
, the type of expr
is narrowed to Thunk<A> | (NonFunction<A> & Function)
. When the condition is false
, the type of expr
is narrowed to NonFunction<A>
.
π Expected behavior
When the condition is true
, the type of expr
should be simplified to just Thunk<A>
.
Thunk<A> | (NonFunction<A> & Function)
= Thunk<A> | ((A extends Function ? never : A) & Function) // (1) by definition
= Thunk<A> | (A extends Function ? never & Function : A & Function) // (2) distributivity
= Thunk<A> | (A extends Function ? never : A & Function) // (3) annihilation
= Thunk<A> | (A extends Function ? never : never) // (4) law of non-contradiction
= Thunk<A> | never // (5) simplification
= Thunk<A> // (6) identity
The challenging step to understand is step number 4 where we invoke the law of non-contradiction to simplify the type A & Function
to never
. The law of non-contradiction states that some proposition P
and its negation can't both be true. In our case, the proposition is Function
. Since A & Function
is in the else branch of the conditional A extends Function
, it implies that A
is not a function. Hence, A
and Function
are contradictory. Thus, by the law of non-contradiction we should be able to simplify it to never
.
In plain English, NonFunction<A> & Function
is a contradiction. A type can't both be a Function
and a NonFunction<A>
at the same time. Hence, the type checker should simplify NonFunction<A> & Function
to never
.
At the very least, NonFunction<A> & Function
should be callable. We shouldn't get a ts2349
error.
Additional information about the issue
I understand that as TypeScript is currently implemented, NonFunction<A> & Function
doesn't simplify to never
for all possible types A
. For example, consider the scenario when A
is unknown
.
NonFunction<unknown> & Function
= (unknown extends Function ? never : unknown) & Function // (1) by definition
= unknown & Function // (2) simplification
= Function // (3) identity
This seems to be because in step number 2 we're simplifying unknown extends Function ? never : unknown
to just unknown
. Instead, we should simplify it to unknown & !Function
where the !
denotes negation, i.e. an unknown value which is not a function. Then we can apply the law of non-contradiction to get the correct type.
NonFunction<unknown> & Function
= (unknown extends Function ? never : unknown) & Function // (1) by definition
= (unknown & !Function) & Function // (2) simplification
= unknown & (!Function & Function) // (3) associativity
= unknown & never // (4) law of non-contradiction
= never // (5) annihilation
At the very least, this seems to suggest the need for some sort of negation type.