I'm trying to express the infinite family of types:
type Q0 = number;
type Q1 = (x: number) => number;
type Q2 = (x: (x: number) => number) => number;
type Q3 = (x: (x: (x: number) => number) => number) => number;
// ...
as a union type. If I define:
type Q = number | ((x: Q) => number);
and a little helper:
type IsSubtypeOf<S, T> = S extends T ? true : false;
I find that:
type _1 = IsSubtypeOf<number, Q>; // => true
type _2 = IsSubtypeOf<(x: number) => number, Q>; // => false
So (x: number) => number isn't an inhabitant of type Q, defying my expectations.
I expected that Q is equivalent to the union of all of its "ground instances".
That is, that Q is the same as:
type Q1
= number
| ((x: number) => number)
| ((x: (x: number) => number) => number)
| ((x: (x: (x: number) => number) => number) => number)
// ...
but in that case, (x: number) => number would be a subtype of Q1.
What am I missing here?
Appendix
I tried "unrolling" Q by adding another ground instance, and this does work (not suprisingly):
type Q2 = number | ((x: number) => number) | ((x: Q2) => number);
type _3 = IsSubtypeOf<(x: number) => number, Q2>; // => true
There is no way to write the infinite union type
in TypeScript. At best you can pick some reasonable maximum length of the union and write a recursive conditional type to achieve it:
In particular, the desired infinite union is not equivalent to
One cannot "push" a union into the parameter type for a function and keep things the same. Functions are contravariant in their parameter types. (See Difference between Variance, Covariance, Contravariance, Bivariance and Invariance in TypeScript .) Unions turn into intersections and vice-versa. So
NotQwould be expanded towhich is equivalent to
And this does not even begin to resemble your desired infinite union.
For a more concrete reason why you cannot assign an
(x: number) => numbertoNotQ, you can see what happens if it were allowed:Here we have made the bad assignment. According to the definition of
NotQ, ifqisn't anumberthen it must be a((x: NotQ) => number). And in that case you would be allowed to callq(q):Of course if you actually run that, you'll get the runtime error that
x.toFixedis not a function. Becauseqis actuallyf, which assumes that its input is anumber, and therefore has atoFixed()method. But you've passed inq, not a number, and thusq(q)explodes. We've lied to the compiler about the type of thingqreally is, and we've been penalized for it.Playground link to code