1

I have the following TypeScript declaration which is producing confusing results.

class C<T extends {}> {
    method() {
        type X = T extends {} ? true : false;
        //   ^? type X = T extends {} ? true : false;
        // Why is X not `true`?
    }
}

Playground

It's treating T as if it's an unknown entity, and I struggle to get any useful type information from it.

For example, trying to inspect property types doesn't work either:

class C<T extends { foo: number }> {
    method() {
        type X = T['foo'];
        //   ^? type X = T['foo'];
        // Why is X not `number`?
    }
}

Strangely, the types are correctly evaluated when its time to assign something to them:

const a: X = 5 // works correctly
const b: X = "str" // fails correctly

What is going on here?

Slava Knyazev
  • 5,377
  • 1
  • 22
  • 43
  • 1
    "*Why is X not `number`?*" `new C<{ foo: 42 }>` – VLAZ Feb 07 '23 at 21:16
  • Conditional types can do all sorts of crazy things, and generic conditional types are essentially just deferred because it's too complicated to try to determine in advance what, if anything, a generic conditional type might resolve to or be re-constrained to. There are various feature requests for this in various states (see [ms/TS#52144](https://github.com/microsoft/TypeScript/issues/52144) for example). And `X` might be narrower than `number` in your second case. Does that fully address your question? If so I could write up an answer explaining; if not, what am I missing? – jcalz Feb 07 '23 at 21:23
  • @jcalz I understand. It's kind of messing with my head that "extending" a type can actually narrow it: https://tsplay.dev/NDe51m – Slava Knyazev Feb 07 '23 at 21:52
  • @SlavaKnyazev I wrote an answer about `extends` working counterintuitively, particularly about unions and why `A extends (A|B)` instead of the other way around: https://stackoverflow.com/q/70559511/1426891 – Jeff Bowman Feb 07 '23 at 23:01
  • I will try to write up an answer tomorrow (it's close to bedtime in my time zone right now) – jcalz Feb 08 '23 at 04:30

1 Answers1

2

Conditional types that depend on a generic type parameter T can, in general, have arbitrarily complex behavior. Even something simple-looking like T extends AAA ? BBB : CCC can have interesting results due to distribution over unions in T. Currently the compiler just completely defers evaluation of such types until such time as the generic type arguments are specified... that is, when the type is no longer generic. Deferred generic conditional types are essentially opaque to the compiler; it can't really tell what values might or might not be assignable to them, so it will tend to complain if you try (e.g., return someValue inside a function whose return type is a generic conditional type).

It would be nice if the compiler could eagerly resolve generic conditional types in cases where there is enough information about the generic type parameter to do so, but that's not what happens. If the question is "why", the answer is that it's just too difficult to do it in a way that works well enough to be useful and without degrading compiler performance too much.

This has been the subject of quite a few issues in GitHub, and inside these issues are the closest thing I've found to an official answer as to why it's like this:

  • microsoft/TypeScript#52144 "Resolve deferred conditional types to their true branch when instantiated with a type parameter constrained to the tested type". This is currently an open feature request. According to this comment

    The reason it's deferred is that, while it would be safe to resolve to the true branch, for a generic it's not safe to resolve to the false branch because the type parameter might be instantiated with some more-specific type that actually does go to the true branch.

    We don't yet have a concept of a "partial deferral"; it would be interesting to try. It'd be pretty tricky.

  • microsoft/TypeScript#48243 "Conditional type doesn't go to true or false branch.". This is closed as a Working as Intended. According to this comment:

    Conditional types are not necessarily linear (meaning that they have predictable behavior between T and an a subtype of T), and figuring out whether or not they are requires reasoning in the form of "Does any type exist such that this type would behave in the other way?", which is not very tractable.

There are undoubtedly more, but hopefully that's enough to convey the reasoning behind the current behavior.


As for the other part of the question with indexed accesses in generics, the reason that stays deferred is because almost any type has subtypes, including number (numeric literal types like 0.5 and 42 exist), so if T extends {foo: number}, its foo property might be narrower than number, and resolving T["foo"] to number would destroy such information.

jcalz
  • 264,269
  • 27
  • 359
  • 360