1

I have the following code:

class Foo<T> {
  x?: T extends string ? string | Foo<T> : Foo<T>
}

function bar<T>(): Foo<T> {
  const x: Foo<T> = { }
  return { x }
}

Why does the compiler yield the following error for return { x }?

Type 'Foo<T>' is not assignable to type 'T extends string ? string | Foo<T> : Foo<T>'. ts(2322)

Foo<T> should satisfy both string | Foo<T> and Foo<T>, so it should also satisfy the type of Foo.x regardless of whether T extends string or not, right?

Milack27
  • 1,619
  • 2
  • 20
  • 31
  • It's a current limitation of TypeScript. The compiler completely *defers* the evaluation of a distributive generic conditional type (like `T extends string ? ... : ...`), so it has no idea what kinds of value might be assignable to it. See [ms/TS#46429](https://github.com/microsoft/TypeScript/pull/46429) for a pull request which implemented the current behavior; performance is just too terrible if the compiler tries to do this. – jcalz Jun 14 '22 at 18:25
  • So that's the answer to the question as asked. If you want a workaround I'd refactor any conditional types where the left and right overlap to move the overlap out, like [this playground link](https://tsplay.dev/NV4k5W) shows. Does this fully address the question? If so I'll write up an actual answer post. If not, what am I missing? – jcalz Jun 14 '22 at 18:25
  • @jcalz As for the refactor you suggested, it unfortunately doesn't work for my case, since the property definition is actually written in a third-party library. So I'll be forced to do something like `return { x: { } as any }`. – Milack27 Jun 14 '22 at 19:01

1 Answers1

2

I agree that, no matter what T is, the type Foo<T> should be assignable to T extends string ? string | Foo<T> : Foo<T>. This is actually a little tricky to verify, since if T is a union type (e.g., number | Date), then Foo<T> will not be a union type (e.g., Foo<number | Date>), but T extends string ? string | Foo<T> : Foo<T> will be a union type (e.g., Foo<number> | Foo<Date>). That's because T extends string ? ... : ... is a distributive conditional type which splits unions into their members before evaluation, and reunites into a new union afterward. So the fact that the conditional type is always assignable to Foo<T> independently of T depends specifically on the definition of Foo<T>, and the fact that it is covariant in T (see this Q/A for a discussion of variance).

But the compiler does not see this. Why?


Well, generally speaking, the evaluation of conditional types that depend on generic type parameters is deferred by the compiler. There was some work done in microsoft/TypeScript#46429 to allow a type to be assignable to a conditional type if it was assignable to both the true and false branches of that type, but this only works for non-distributive types that don't use the infer keyword. Since you have a distributive type, it doesn't work here.

So, evaluation of T extends string ? string | Foo<T> : Foo<T> is deferred until such time as T is specified. Inside the body of bar(), T is unspecified, so the conditional type is not evaluated there. It is essentially opaque to the compiler, and it won't be able to verify that you can assign a value to it, unless that value is also of the identical conditional type. And Foo<T> is not. So the compiler complains.


Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360