Take the following snippet of code:
type A = {a: unknown}
type TakesA<T extends A> = unknown
type B = { b: unknown }
type TakesB<T extends B> = unknown
type AB = A | B
type TakesAB<T extends AB> = T extends A ? TakesA<T> : TakesB<T>
I get the error Type 'T' does not satisfy the constraint 'B'. Type 'AB' is not assignable to type 'B'
. However, shouldn't the conditional type have narrowed down the type of T
to B
in the else-clause? Why does this behaviour occur, and how should I work around it?
CodePudding user response:
Here's another — more simplified — example of the same issue:
type Example<T extends string | number> = T extends number
? T['toFixed']
: T['length']; /*
~~~~~~~~~~~
Type '"length"' cannot be used to index type 'T'.(2536) */
We expect TypeScript to narrow T
to string
in the false/else branch, but that does not happen (as of TS version 4.9.3
at least).
This is explained in the GitHub issue at microsoft/TypeScript#29188:
jack-williams commented on Jan 3, 2019:
Conditional types do not produce substitution types for the false branch of the conditional (a.k.a do not narrow in the false branch).
There was an attempted fix here: #24821, however this was closed.
...
weswigham commented on Jan 3, 2019:
@jack-williams had identified the core of the issue. We use "substitution" types internally to track the constraints applied to a type within the
true
branch of a conditional, however we do no such tracking for thefalse
branch. This means that you can't actually bisect a union type with a conditional right now, as @lodo1995 points out, you must chain two conditions and invert the check so your logic is in thetrue
branch instead.Part of the reason why we didn't move forward with #29011 (other than one of the relations I identified not holding up under scrutiny) is that tracking falsified constraints with substitution types kinda works... but when you perform the substitution, the information is lost, since we do not currently have a concept of a "negated" type (I mitigated this a little bit by remateriaizling the substitutions that tracked negative constraints late, but that's a bit of a hack). We cannot say that the given
T extends string ? "ok" : T
that the type ofT
in the false branch is aT & ~string
, for example - we do not have the appropriate type constructors currently.We regularly bring up how we really do probably need it for completeness, but the complexity "negated" types bring is... large? At least that's what we seem to think - it's not immediately obvious that a
~string
is an alias for "any type except those which are or extend string", and therefore that a~string & ~number
is "any type except strings or numbers" (note how despite the use of&
, the english you read as used the word "or").So we're very aware of what needs to be done to make this work better... we're just having trouble convincing ourselves that it's "worth it".
So — in summary — the advice from Dimava is sound, you should use this type, with an unconditional never
result for the final "unreachable" branch:
type TakesAB<T extends AB> = T extends A
? TakesA<T>
: T extends B
? TakesB<T>
: never;