The limits of TypeScript's Template Literal types

🛈 The code was tested against TypeScript 4.5.4 only. Other versions may have different errors or behavior.

Template Literal Types

Template literal types allow you to do things like,

type World = "world";
type Greeting = `hello ${World}`;

Here Greeting is equivalent to the type "hello world".

🛈 The Range<a, b> type being used below is from Guillaume Mastio's Stackoverflow answer.

You can also use union types in template literals, allowing you to succinctly define huge unions,

type YYYY = `20${Range<0, 10>}${Range<0, 10>}`;

Here YYYY is equivalent to the following union,

"2000" | "2001" | /* ... */ | "2098" | "2099"

And combinatorial explosions occur when you do something like,

type YYYY = `20${Range<0, 10>}${Range<0, 10>}`
type MM = `0${Range<1, 10>}` | `${Range<11, 13>}`
type DD = `0${Range<1, 10>}` | `${Range<11, 32>}`
type IsoDate = `${YYYY}-${MM}-${DD}`

IsoDate is a huge union containing 100 * 12 * 31 = 37200 strings.


We can start to see limits of the TypeScript type checker if we push things a bit further.

First, consider a simple range of numbers,

type T999 = Range<0, 999>
type T1000 = Range<0, 1000>

Here T999 is a perfectly valid type. But T1000 is invalid,

[tsserver 2589] [E] Type instantiation is excessively deep and possibly infinite.

So the limit for the recursively defined Range type is 1000.

If we look at the template literal type, we can see it hits a limit too,

type T316 = `${Range<0, 316>}-${Range<0, 316>}`
type T317 = `${Range<0, 316>}-${Range<0, 317>}`

Here T316 (316 === Math.foor(Math.sqrt(100_000))) is a valid type with just under 100k items. If we go above 100k with T317, we hit a limit,

[tsserver 2590] [E] Expression produces a union type that is too complex to represent.