TypeScript: Intersection of conditional types, acting on a generic type, with another type doesn't narrow the generic type
đ 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.
About this issue
- Original URL
- State: closed
- Created 5 months ago
- Comments: 38 (4 by maintainers)
#4196
For arbitrary
TandU, âTis not a subset ofUâ does not imply âTis a set which contains no elements fromUâ.T & !Umeans the latter.@aaditmshah counterexample: Open in Playground
More concretely:
You asked whether
Animalwas assignable toDog. Itâs true that it isnât. If you then narrowTtoT & !Dogin the false branch, youâre asserting thatDogis not assignable to the originalT, which is false.@somebody1234 Other languages donât have conditional types like TypeScript does. So, no that analogy doesnât apply.
I think I might see where youâre coming from here; you envision the
Aas being narrowed in the false branch due to theextendscheck, making it not an identity in that context. Even so, what you propose is still incorrect:T = A & !Functionimplies thatF extends Tis false for all typesF extends Function, which is something that was not checked in the conditional type. So the proposed narrowing is still wrong, even if negated types existed.tl;dr is that, in general,
T extends Ubeing true or false only tells you whetherTis assignable toUbut says nothing about whetherUis assignable toT. So it doesnât make sense to setT = T & !Uin the false branch of said check.Iâm really not sure how you can see
and interpret that as anything other than
because thatâs exactly what that type means. Itâs literally an identity function if you pass it anything thatâs not currently assignable to
Function. The fact that you intersect the resulting type withFunctionafterwards is irrelevant.This is precisely what I wrote in my original post.
In fact, if we had negation types then as you pointed out we could simply define
NonFunction<A>asA & !Function. Hence, we should have a discussion about negation types. To that end, Iâll close this issue and mark it as a duplicate of #4196.Anyway, now that we agree on something, could you at least tell me what are your thoughts on narrowing in the false branch of conditional types if negated types were added to TypeScript?
Logically, it is a defect. You wouldnât expect the true branch of the following
ifstatement to be evaluated right?After this discussion, Iâm convinced that not having negation types in TypeScript is indeed a defect. Itâs like a baby born without hearing. Deafness is certainly a defect. Similarly, not having negation types in TypeScript is also a defect.
Anyway, Iâm closing this issue as a duplicate of #4196.
I understand that the solution this for this problem might take a lot of work to implement, and therefore itâs not something that you want to solve. However, this is indeed a defect of the type system as I demonstrated using propositional logic above. Hence, I donât think the âNot a Defectâ label is justified here.
To be fair, I can easily solve this problem using type guards.
However, I expected TypeScript to correctly narrow the type of
exprtoThunk<A>even without type guards. Itâs quite obvious that an inhabitant ofExpr<A>can only be a function if itâs an inhabitant ofThunk<A>.Yes,
NonFunction<Obj>âreturnsâObjcurrently but thatâs a defect of the type system, which is why I created this issue.The type
NonFunction<Obj>should returnObj & !Function. You donât need to go back and re-evaluate the conditional type. Just make the conditional type returnObj & !Function.By simplifying
NonFunction<Obj>to justObj, weâre losing type information. In particular, weâre losing the information that we checked whetherObjis a function and we found out that it isnât a function. The check is the important part here.True, if we had negated types then I could define
NonFunctionas follows, which by the way is oh so perfect.However, I disagree with the second part of your statement. A conditional type of the form
A extends B ? never : Ashould simplify toA & !Bfor all typesAandB. And, hereâs the proof.First, we need to understand what
A extends Bdenotes. Turns out,extendsdenotes material implication. IfAextendsBthen we should be able to substitute a value of typeAeverywhere a value of typeBis expected. That is,Ashould be âassignableâ toB. In propositional logic, we would write this as $A \to B$.Next, the
nevertype denotes falsehood and is represented as $\bot$ in propositional logic. Finally, any conditional typeP ? Q : Rcan be represented as the conjunction of two conditionals $(P \to Q) \land (\neg P \to R)$ in propositional logic.Putting it all together, the conditional type
A extends B ? never : Awould be represented in propositional logic as $((A \to B) \to \bot) \land (\neg (A \to B) \to A)$. However, $P \to \bot$ is the same as $\neg P$. Hence, the proposition can be simplified to $\neg (A \to B) \land (\neg (A \to B) \to A)$.At this point, we can use the modus ponens rule to infer that $A$ is a true proposition. However, inference is not the same as simplification. Yes, $(\neg (A \to B) \land (\neg (A \to B) \to A)) \to A$ is a true proposition. However, $\neg (A \to B) \land (\neg (A \to B) \to A) \neq A$. Since, $P \land (P \to Q)$ simplifies to $P \land Q$, we can simplify $\neg (A \to B) \land (\neg (A \to B) \to A)$ to $\neg (A \to B) \land A$.
Finally, $\neg (A \to B) \land A$ can be simplified to $\neg (\neg A \lor B) \land A$ because $P \to Q$ is equivalent to $\neg P \lor Q$. Next, by De Morganâs law we can further simplify $\neg (\neg A \lor B) \land A$ to $(A \land \neg B) \land A$. And, now we can trivially see that $(A \land \neg B) \land A$ is just $A \land \neg B$.
Now, you might be think that thereâs some flaw in my argument because Iâm treating types as propositions. After all, types are not the same as propositions. But, the CurryâHoward isomorhpism is proof that types and propositions are indeed equivalent. Itâs one of the seminal theorems of computer science.
In conclusion,
A extends B ? never : Ashould simplify toA & !B. Inference is not the same as simplification. Yes,A extends B ? never : AimpliesA. However, it also implies!B.But thatâs the thing - those are the same type. Type aliases are just type-functions -
NonFunction<Obj>âreturnsâObj, so in the end you are just doingObj & Fun. TS doesnât go back and reevaluate the conditional type every time you manipulate it - thatâs not how they work.