Reputation: 8631
For reasons not relevant to the question (but which include fun and profit in type-level programming), one of my types eventually boils down to the following minimal example:
type IsTrue<A extends true> = A
type Refl<M> = M extends M ? true : false
type Proof<M> = IsTrue<Refl<M>>
... which results in a compile error. Now, we can discuss how I ended up here, and it is related to a (probably) wrong way of encoding Type Equality. But the question remains: When doesn't M extends M
resolves to true
for all M
? What's the counter-example? How would one go to fix this (possibly by constraining M
)?
Upvotes: 3
Views: 5369
Reputation: 329408
Conditional types, as originally implemented in microsoft/TypeScript#21316 are not always evaluated eagerly. If they depend on an as-yet-unspecified generic type parameter, like the M
inside the body of the Refl<M>
definition, the compiler will generally defer evaluation of the conditional type. Note that the details about exactly when and where the compiler will evaluate a conditional type are not spelled out in any documentation, and they've been evolving over time with new releases of TypeScript, so I can't say anything with absolute certainty here. But the general situation is as I've described it.
You were expecting the compiler to look at M extends M ? true : false
and eagerly reduce it to true
so that your definition would be equivalent to type Refl<M> = true
, either inside the definition of Refl<M>
or inside the definition of Proof<M>
. Neither of these evaluations happen; they are deferred because in both cases M
is an unspecified type parameter. So the compiler cannot be sure that Refl<M>
will be any narrower than the union true | false
(also known as the boolean
type), and thus is not known to satisfy the constraint for IsTrue<A extends true>
.
So it isn't that M extends M ? true : false
should ever actually evaluate to false
(as far as I know it can't), but that the compiler fails to evaluate it at all in order to simplify it to true
.
I don't see any GitHub issues about this specific circumstance, but there are many such issues which boil down to the compiler's inability to analyze conditional types that depend on an unresolved generic type parameter. For a relatively recent example, see microsoft/TypeScript#46795.
Note that the particular form M extends ... ? ... : ...
where M
is a plain generic type parameter is known as a distributive conditional type and so any unions in M
would be broken into individual members before being evaluated. This doesn't affect whether Refl<M>
could ever be wider than true
, but it can affect the output type:
type Refl<M> = M extends M ? true : false
type Hmm = Refl<never> // never
type Refl2<M> = [M] extends [M] ? true : false;
type Hmm2 = Refl2<never> // true
Refl<M>
is distributive over unions in M
, and never
is considered to be "the empty union" (see this comment in ms/TS#23182) and thus the output is also the empty union. But Refl2<M>
is not distributive (since [M]
is not a plain generic type parameter) and so Refl2<never>
is true
. Both never
and true
are assignable to true
, though, so IsTrue<Refl<M>>
would work out no matter what. But it's trickier than it might seem to demonstrate that.
It is conceivable that a feature could be introduced whereby conditional types of the form X extends X ? Y : Z
could be eagerly reduced to Y
in cases where Y
does not depend on X
(your case) or where X
is not a plain generic type parameter (not your case). But such a feature would have a negative effect on compiler performance, since it would need to check every conditional type for this situation even though the vast majority of conditional types are not like this; features need to pay for themselves, and this one probably wouldn't. Even worse, there's probably a lot of real-world code that either intentionally or unintentionally depends on the compiler deferring conditional types like this, and such a feature would be a large breaking change.
Finally, if you're just interested in a workaround, my usual approach is as follows: If you're sure that T extends U
but the compiler isn't, then you can't use T
in a place that expects something assignable to U
. But you can use Extract<T, U>
. The Extract<T, U>
utility type is primarily meant to filter any unions in T
so that only those members assignable to U
are left. But the compiler does see Extract<T, U>
is assignable to both T
and U
(possibly in ms/TS#29437), and if you're right that T extends U
, then Extract<T, U>
will eventually evaluate to just T
as desired. So this off-label use of Extract
does what we want:
type Proof<M> = IsTrue<Extract<Refl<M>, true>> // okay
This is almost like a type-level type assertion, so similar caveats apply. If you're wrong about Refl<M>
being assignable to true
, then IsTrue<Extract<Refl<M>, true>>
would still compile, but you're no longer evaluating IsTrue<Refl<M>>
, but something more like IsTrue<never>
or IsTrue<true>
. So be careful!
Upvotes: 2