15.6 Limitations and Performance Considerations
Now, let’s talk about the walls we inevitably hit. Template Literal Types are powerful, but they’re not magic pixie dust. The compiler has to work for its supper, and sometimes, its appetite isn’t as big as your ambition. Understanding these limits is what separates a clever type architect from someone who just throws infer at the wall until something sticks.
The Hard Ceiling: String Length
The most immediate and hilarious limitation you’ll encounter is the maximum allowed string length for a template literal type. The TypeScript compiler will simply give up if you generate a string that’s too long. It’s not a gentle error; it’s a hard crash. The exact limit is a bit fluid and depends on the complexity of the type, but it’s in the low tens of thousands of characters.
Why? Because the compiler is essentially building and traversing a massive union type, and doing that for a string like "a" | "aa" | "aaa" | ... up to 10,000 characters is a fantastic way to make your IDE beg for mercy. It’s a performance safeguard to prevent you from accidentally (or intentionally) creating types that would bring the language server to its knees.
// Don't try this at home. Seriously.
type InfiniteString<S extends string> = S | `${InfiniteString<`a${S}`>}`;
// Error: Type instantiation is excessively deep and possibly infinite.
// (or your computer might just freeze)
A more realistic pitfall is trying to parse something like a long CSV row. You’ll hit the ceiling fast.
type ParseCsvRow<Row extends string> = ... // A complex type that splits on commas
// For a short row, it's fine.
type Test1 = ParseCsvRow<"a,b,c">; // Works: ["a", "b", "c"]
// For a long row, the compiler taps out.
type Test2 = ParseCsvRow<"a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z">;
// Error: Expression produces a union type that is too complex to represent.
Best Practice: Use template literals for patterns, not for generating massive sets of concrete values. They’re fantastic for defining the shape of a string (e.g., all strings that start with https://), but terrible for enumerating every possible value.
The Performance Cliff
Even if you avoid the hard string length limit, you can easily create types that are technically valid but make your developer experience miserable. The core of the issue is that template literal types often result in massive unions, and unions are a performance bottleneck for the TypeScript compiler.
Every time you use a type like A | B | C, the compiler has to check operations (like conditional types or mapped types) against every single member of that union. This is an exponential problem. A union with 100 members isn’t 100 times slower to check than a single type; it can feel like 100^2 times slower.
// This seems innocent...
type Alpha = 'a' | 'b' | 'c' | 'd' | 'e'; // ...and so on through the alphabet.
// But then you do this:
type WithPrefix = `user-${Alpha}`; // Now you have a 26-member union.
// And then you use it in a mapped type:
type MappedType = {
[K in WithPrefix]: boolean;
};
// The compiler handles this. 26 members? Easy.
// Now imagine your source union has 1000 members. Your mapped type has 1000 properties.
// Your type checking and IntelliSense will become noticeably sluggish.
This is why you’ll feel the pain most acutely in your IDE. Hovering over a type might take seconds instead of milliseconds. Saving a file might trigger a long type check. This isn’t a bug; it’s the compiler doing exactly what you asked, and it’s working very hard to do it.
Best Practice: Profile your types. If you notice your IDE slowing down, use extends clauses and conditional types to narrow the domain of your template literals early, reducing the overall union size the compiler has to deal with downstream.
The Curse of the Generic
Here’s a subtle one that gets everyone. You cannot use a generic type parameter as a template literal pattern. You can only use it as a string value to be inserted.
// What you WANT to do (but can't):
function makeEvent<Event extends string>(event: Event): `on${Capitalize<Event>}` {
return `on${capitalize(event)}` as const; // The "as const" is a clue we're fighting the type system.
}
// Error: Type 'string' is not assignable to type '`on${Capitalize<Event>}`'
// Why? Because the compiler cannot know what `Capitalize<Event>` is at the time of writing the function.
// `Event` is a type, not a value. The template literal type can't be "built" until the function is called.
The workaround is to use a constraint and a lot of type assertions, which is ugly but effective.
function makeEvent<Event extends string>(event: Event): `on${Capitalize<Event>}` {
// You have to do the transformation at the value level with JS and then assert the type.
return `on${event.charAt(0).toUpperCase() + event.slice(1)}` as const;
}
It feels like a hack because it is one. The type system is describing the relationship, but you’re forced to implement it manually in JavaScript. This is a fundamental gap between the world of types (which are erased) and values (which exist at runtime). Template literals live in both worlds, and sometimes the seam between them shows.