简体   繁体   中英

When does A not extends A in TypeScript

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 )?

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 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 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 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... ? ... : ... 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 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!

Playground link to code

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM