-
Notifications
You must be signed in to change notification settings - Fork 13.1k
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) identityThe 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) identityThis 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) annihilationAt the very least, this seems to suggest the need for some sort of negation type.