π 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
https://www.typescriptlang.org/play?target=99&jsx=0#code/C4TwDgpgBAKgFgVwHYGsA8BBAfFAvFACgEo8cMBuAKEtEigDkB7JAMWQGNgBLZzHfDFAgAPYBCQATAM5Q2STjyRQA-FCQQAbhABOUAFxQK1WtACiwsNr55YiVNYA+DZnIW9sVSu2ZTgQjQCGADYIAWI2fAQilgbmlnxEBoK4WJRQUCaMAGZCFrq4BVAARFkc3MxFKrmWxPrV2lTpAPRN6W3tHZ1dAHq93WlQLVAAtKNj4xOTU9Mzs3PzUwND8Fwy0doQUlKKUKtqjH7swUEBAEZBEAB0S63pTH7HUN5IvlzACOLAMtkZ4NAA5PBkOhsFAnAQmKwyoprAAyWTQ5hEf5QAIbJ7HM4Xa7NW5tGB-KD-SGucpIOEI+RklFwAIyJCMDFBIJQbYAcyQYQQGykl0IACYAMwAFgAnEQbiMFtKZbK5TNJQQwGiAgBbCBibQkdYGIH2UHgkmI8mg+GkxQS3FS+U2212iaUIA
π» 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.
π Search Terms
π Version & Regression Information
This is the behavior in every version I tried, and I reviewed the FAQ for entries about narrowing.
β― Playground Link
https://www.typescriptlang.org/play?target=99&jsx=0#code/C4TwDgpgBAKgFgVwHYGsA8BBAfFAvFACgEo8cMBuAKEtEigDkB7JAMWQGNgBLZzHfDFAgAPYBCQATAM5Q2STjyRQA-FCQQAbhABOUAFxQK1WtACiwsNr55YiVNYA+DZnIW9sVSu2ZTgQjQCGADYIAWI2fAQilgbmlnxEBoK4WJRQUCaMAGZCFrq4BVAARFkc3MxFKrmWxPrV2lTpAPRN6W3tHZ1dAHq93WlQLVAAtKNj4xOTU9Mzs3PzUwND8Fwy0doQUlKKUKtqjH7swUEBAEZBEAB0S63pTH7HUN5IvlzACOLAMtkZ4NAA5PBkOhsFAnAQmKwyoprAAyWTQ5hEf5QAIbJ7HM4Xa7NW5tGB-KD-SGucpIOEI+RklFwAIyJCMDFBIJQbYAcyQYQQGykl0IACYAMwAFgAnEQbiMFtKZbK5TNJQQwGiAgBbCBibQkdYGIH2UHgkmI8mg+GkxQS3FS+U2212iaUIA
π» Code
π Actual behavior
The parameter
expr, which has the typeExpr<A>, is being narrowed by thetypeof expr === "function"condition. When the condition istrue, the type ofexpris narrowed toThunk<A> | (NonFunction<A> & Function). When the condition isfalse, the type ofexpris narrowed toNonFunction<A>.π Expected behavior
When the condition is
true, the type ofexprshould be simplified to justThunk<A>.The challenging step to understand is step number 4 where we invoke the law of non-contradiction to simplify the type
A & Functiontonever. The law of non-contradiction states that some propositionPand its negation can't both be true. In our case, the proposition isFunction. SinceA & Functionis in the else branch of the conditionalA extends Function, it implies thatAis not a function. Hence,AandFunctionare contradictory. Thus, by the law of non-contradiction we should be able to simplify it tonever.In plain English,
NonFunction<A> & Functionis a contradiction. A type can't both be aFunctionand aNonFunction<A>at the same time. Hence, the type checker should simplifyNonFunction<A> & Functiontonever.At the very least,
NonFunction<A> & Functionshould be callable. We shouldn't get ats2349error.Additional information about the issue
I understand that as TypeScript is currently implemented,
NonFunction<A> & Functiondoesn't simplify toneverfor all possible typesA. For example, consider the scenario whenAisunknown.This seems to be because in step number 2 we're simplifying
unknown extends Function ? never : unknownto justunknown. Instead, we should simplify it tounknown & !Functionwhere 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.At the very least, this seems to suggest the need for some sort of negation type.