Fix: TypeScript Template Literal Type Error — Type Not Assignable or Inference Fails
Part of: React & Frontend Errors
Quick Answer
How to fix TypeScript template literal type errors — string combination types, conditional inference, Extract and mapped types with template literals, and common pitfalls.
The Problem
TypeScript rejects a string that should match a template literal type:
type EventName = `on${string}`;
const handler: EventName = 'onClick'; // OK
const handler2: EventName = 'handleClick'; // Error: Type '"handleClick"' is not assignable to type '`on${string}`'Or template literal type inference fails in a generic function:
type PropEventSource<T> = {
on<K extends string & keyof T>(event: `${K}Changed`, callback: (value: T[K]) => void): void;
};
// Error: Argument of type '"nameChanged"' is not assignable to parameter of type
// `${string & keyof T}Changed`Or combining union types in a template literal produces an unexpected result:
type Color = 'red' | 'blue' | 'green';
type Size = 'sm' | 'md' | 'lg';
type ButtonVariant = `${Color}-${Size}`; // Should be 9 combinations
// 'red-sm' | 'red-md' | 'red-lg' | 'blue-sm' | ...
const btn: ButtonVariant = 'red-xl'; // Error — 'xl' is not in SizeWhy This Happens
TypeScript template literal types were introduced in 4.1. They have specific inference rules and limitations that interact in non-obvious ways with the rest of the type system, and several of the rules have shifted between releases.
- No substring matching —
`on${string}`only matches strings that start with"on". TypeScript doesn’t check if a given string satisfies a pattern at runtime; it only checks structural compatibility at compile time. - Union distribution — when you use a union in a template literal type, TypeScript distributes the union and creates all combinations. This is powerful but can create very large union types.
- Inference in generics requires the right constraints — TypeScript can infer template literal components from concrete strings, but the generic constraints must be set up correctly for inference to work.
stringin template literals vs union of string literals —`prefix-${string}`is a broad type accepting any string after the prefix.`prefix-${'a' | 'b'}`is a specific union of two strings.
Most importantly, the inference power keeps improving across TS releases. A pattern that produced “Type instantiation is excessively deep and possibly infinite” on TS 4.5 may compile cleanly on TS 5.4. Pinning your typescript version matters for the answer to this article.
TypeScript Template Literal Type History — What Shipped When
- TS 4.1 (November 2020) introduced template literal types, the four intrinsic string types (
Uppercase,Lowercase,Capitalize,Uncapitalize),inferin template literal patterns, and key remapping in mapped types (asclause). Everything in this article exists from 4.1 onward — earlier versions throw “type alias ‘X’ circularly references itself” on most of these patterns. - TS 4.2 (Feb 2021) added smarter type alias preservation in error messages — template literal errors are easier to read because aliases like
EventNameno longer expand to their full union. - TS 4.4 (Aug 2021) added control-flow analysis for aliased conditions, which helps narrow template literal types after a type guard.
if (typeof x === 'string' && x.startsWith('on'))now narrows to the template literal alias inside the block. - TS 4.5 (Nov 2021) added the
Awaited<T>type and improved tail-recursion elimination on conditional types. Recursive template literal types (likeExtractParamsin Fix 4 below) can now go much deeper before hitting the recursion limit. - TS 4.9 (Nov 2022) introduced the
satisfiesoperator — invaluable for template literal types because you can verify that a literal object matches a type without widening its inferred type. Use it instead of an explicit annotation when you want both type-checking and the narrowest inferred type. - TS 5.0 (March 2023) added
consttype parameters (function fn<const T>(x: T)). Combined with template literal inference, this is how libraries pin the exact literal string passed in rather than widening tostring. - TS 5.2 (Aug 2023) added explicit resource management (
usingkeyword) — not directly related, but the same release improved inference forArray.fromandString.rawcallers that rely on template literal types. - TS 5.4 (March 2024) introduced
NoInfer<T>. This is the cleanest way to force one type parameter to drive inference while leaving another to be checked against the inferred value — fixes a whole category of “TS infersstringinstead of my literal union” bugs. - TS 5.5 (June 2024) added inferred type predicates (
array.filter(x => x !== null)now narrows toNonNullable<T>[]without an explicit predicate). For template literal types this matters because filters that testtypeof x === 'string'now keep the literal type intact through the filter call. - TS 5.6 (Aug 2024) improved checking of
if (typeof x === 'undefined')style guards and stricter check for “always truthy” expressions — helps catch dead-code branches in conditional template literal types.
If a Stack Overflow answer says “this used to error but now works,” it was usually written against pre-4.5 TypeScript. Check your tsc --version before assuming the article is right.
Fix 1: Understand What Template Literal Types Match
Template literal types describe the shape of a string, not arbitrary patterns:
// Basic template literal types
type Greeting = `Hello, ${string}`;
// Matches: "Hello, world", "Hello, Alice", "Hello, "
// Does NOT match: "Hi, world", "hello, world"
type EventHandler = `on${Capitalize<string>}`;
// Matches: "onClick", "onChange", "onKeyDown"
// Does NOT match: "onclick" (lowercase 'c')
type CSSProperty = `--${string}`;
// Matches CSS custom properties: "--primary-color", "--font-size"
// Intrinsic string manipulation types
type Upper = Uppercase<'hello'>; // 'HELLO'
type Lower = Lowercase<'WORLD'>; // 'world'
type Cap = Capitalize<'foo'>; // 'Foo'
type Uncap = Uncapitalize<'Foo'>; // 'foo'
// Combining with unions
type Direction = 'top' | 'right' | 'bottom' | 'left';
type MarginProp = `margin-${Direction}`;
// 'margin-top' | 'margin-right' | 'margin-bottom' | 'margin-left'
const margin: MarginProp = 'margin-top'; // OK
const margin2: MarginProp = 'margin-center'; // Error: not in unionFix 2: Use Template Literals for Event System Typing
A common use case — type-safe event names derived from data shapes:
type Person = {
name: string;
age: number;
email: string;
};
// Generate change event names from object keys
type ChangeEventName<T> = {
[K in keyof T]: `${string & K}Changed`;
}[keyof T];
type PersonEvents = ChangeEventName<Person>;
// 'nameChanged' | 'ageChanged' | 'emailChanged'
// Event emitter with typed events
type EventMap<T> = {
[K in keyof T as `${string & K}Changed`]: (newValue: T[K], oldValue: T[K]) => void;
};
type PersonEventMap = EventMap<Person>;
// {
// nameChanged: (newValue: string, oldValue: string) => void;
// ageChanged: (newValue: number, oldValue: number) => void;
// emailChanged: (newValue: string, oldValue: string) => void;
// }
class TypedEmitter<T> {
private handlers: Partial<EventMap<T>> = {};
on<K extends keyof T>(
event: `${string & K}Changed`,
handler: (newValue: T[K], oldValue: T[K]) => void
): void {
(this.handlers as any)[event] = handler;
}
emit<K extends keyof T>(event: `${string & K}Changed`, newValue: T[K], oldValue: T[K]): void {
const handler = (this.handlers as any)[event];
handler?.(newValue, oldValue);
}
}
const emitter = new TypedEmitter<Person>();
emitter.on('nameChanged', (newName, oldName) => {
console.log(`Name changed from ${oldName} to ${newName}`);
});
// newName and oldName are typed as stringFix 3: Fix Inference in Generic Functions
Template literal inference requires the type parameter to be constrained correctly. On TS 5.4+, NoInfer makes the intent explicit and produces better error messages.
// WRONG — TypeScript can't infer K from the template literal argument
function subscribe<T, K extends keyof T>(
obj: T,
event: `${K}Changed`, // TypeScript struggles to infer K from this
callback: (value: T[K]) => void
): void { }
subscribe(person, 'nameChanged', (value) => {}); // Error
// CORRECT — use a string & constraint to help inference
function subscribe<T, K extends string & keyof T>(
obj: T,
event: `${K}Changed`,
callback: (value: T[K]) => void
): void { }
subscribe(person, 'nameChanged', (value) => {
console.log(value.toUpperCase()); // value is typed as string
});
// TS 5.4+ — use NoInfer to drive inference from the obj parameter only
function subscribe2<T, K extends string & keyof T>(
obj: T,
event: `${NoInfer<K>}Changed`, // K is inferred from elsewhere, not this slot
callback: (value: T[K]) => void
): void { }
// TS 5.0+ — use const type parameters to keep literal types narrow
function emit<const E extends string>(event: `on${Capitalize<E>}`): E {
return event.replace(/^on/, '').toLowerCase() as E;
}Fix 4: Extract Parts from Template Literal Types
Use infer inside conditional types to extract parts of a template literal:
// Extract the HTTP method from a route string like "GET /users"
type ExtractMethod<T extends string> =
T extends `${infer Method} ${string}` ? Method : never;
type M = ExtractMethod<'GET /users'>; // 'GET'
type M2 = ExtractMethod<'POST /orders'>; // 'POST'
// Extract route parameters like "/users/:id/posts/:postId"
// Tail-recursion eliminator added in TS 4.5 makes this practical for long paths
type ExtractParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParams<`/${Rest}`>
: T extends `${string}:${infer Param}`
? Param
: never;
type Params = ExtractParams<'/users/:userId/posts/:postId'>;
// 'userId' | 'postId'
// Build a route params object type
type RouteParams<T extends string> = {
[K in ExtractParams<T>]: string;
};
type UserPostParams = RouteParams<'/users/:userId/posts/:postId'>;
// { userId: string; postId: string }
function createRoute<T extends string>(
path: T,
params: RouteParams<T>
): string {
let result: string = path;
for (const [key, value] of Object.entries(params)) {
result = result.replace(`:${key}`, value as string);
}
return result;
}
const url = createRoute('/users/:userId/posts/:postId', {
userId: '123', // Required, TypeScript enforces it
postId: '456', // Required
// missing: 'abc' // Error — 'missing' is not a param
});
// '/users/123/posts/456'Fix 5: Mapped Types with Template Literals
Create utility types that transform object keys:
// Add 'get' prefix to all methods
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type PersonGetters = Getters<Person>;
// {
// getName: () => string;
// getAge: () => number;
// getEmail: () => string;
// }
// Add 'set' prefix
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};
// Generate both getters and setters
type Accessors<T> = Getters<T> & Setters<T>;
// Remove event handler prefixes
type RemovePrefix<T, Prefix extends string> = {
[K in keyof T as K extends `${Prefix}${infer Rest}` ? Uncapitalize<Rest> : K]: T[K];
};
type Events = {
onClick: (e: MouseEvent) => void;
onChange: (e: Event) => void;
onFocus: () => void;
};
type CleanEvents = RemovePrefix<Events, 'on'>;
// {
// click: (e: MouseEvent) => void;
// change: (e: Event) => void;
// focus: () => void;
// }Fix 6: CSS-in-TypeScript with Template Literal Types
Type CSS values and class names. The satisfies operator from TS 4.9 is perfect here because it lets you both check the constraint and keep the narrowest inferred type:
// Type-safe CSS custom properties
type CSSVariables = {
'--primary-color': string;
'--font-size-base': string;
'--spacing-unit': string;
};
function setCSSVar<K extends keyof CSSVariables>(
element: HTMLElement,
variable: K,
value: CSSVariables[K]
): void {
element.style.setProperty(variable, value);
}
// Only valid CSS variable names accepted
setCSSVar(document.body, '--primary-color', '#3b82f6'); // OK
setCSSVar(document.body, '--invalid', 'red'); // Error
// Type-safe Tailwind-style class generation
type Breakpoint = 'sm' | 'md' | 'lg' | 'xl' | '2xl';
type SpacingScale = '0' | '1' | '2' | '4' | '8' | '16';
type ResponsiveClass<
Prefix extends string,
Value extends string
> = `${Prefix}-${Value}` | `${Breakpoint}:${Prefix}-${Value}`;
type PaddingClass = ResponsiveClass<'p', SpacingScale>;
// 'p-0' | 'p-1' | 'p-2' | ... | 'sm:p-0' | 'md:p-4' | ...
const className: PaddingClass = 'md:p-4'; // OK
const wrong: PaddingClass = 'p-3'; // Error — '3' not in SpacingScale
// TS 4.9+ — satisfies keeps the exact literal type while checking the constraint
const theme = {
primary: 'p-4',
secondary: 'sm:p-8',
} satisfies Record<string, PaddingClass>;
// theme.primary has type 'p-4', not PaddingClass — useful downstreamStill Not Working?
Template literal types with string are too broad — `prefix-${string}` accepts any string starting with “prefix-”. If you need to narrow it, replace string with a specific union: `prefix-${'a' | 'b' | 'c'}`.
Large union types cause performance issues — if your template literal combines two large unions (e.g., 50 × 50 = 2500 combinations), TypeScript may slow down or hit type instantiation limits. Use Extract to narrow the union at the point of use rather than computing all combinations upfront.
Template literal inference doesn’t work with function overloads — TypeScript’s template literal inference works best with single generic parameters. If you have multiple interacting type parameters, consider splitting the function or using conditional types to guide inference.
as const for string literal inference — when a variable is assigned a string, TypeScript widens it to string. Use as const to keep the literal type:
const event = 'onClick'; // Type: string — too wide
const event2 = 'onClick' as const; // Type: 'onClick' — literal“Type instantiation is excessively deep and possibly infinite” — your recursive template literal pattern is hitting the 50-level depth limit. The TS 4.5 tail-recursion improvements help if you can rewrite the recursion to be in tail position (the recursive ExtractParams call must be the last thing in the conditional). If that’s not possible, split the recursion into a helper that processes one segment per call.
Library types regress after upgrading TypeScript — TS minor versions tighten inference rules. tsc 5.4 rejects some patterns that tsc 5.0 accepted. Lock your typescript devDependency to a known version, and bump it deliberately when you have time to fix the new errors. Use "typescript": "~5.5.0" (tilde, not caret) to take patch fixes without minor surprises.
Editor uses a different TypeScript version than CLI — VS Code defaults to its bundled TypeScript, which lags behind node_modules/typescript. Run “TypeScript: Select TypeScript Version → Use Workspace Version” once per workspace, and check the version in the status bar.
For related TypeScript issues, see Fix: TypeScript Mapped Type Error, Fix: TypeScript Generic Constraint Error, Fix: TypeScript Conditional Types Not Working, and Fix: TypeScript Type Not Assignable.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: TypeScript Conditional Types Not Working — infer Not Extracting, Distributive Behavior Unexpected, or Type Resolves to never
How to fix TypeScript conditional type issues — infer keyword usage, distributive conditional types, deferred evaluation, naked type parameters, and common conditional type patterns.
Fix: TypeScript Discriminated Union Error — Property Does Not Exist or Narrowing Not Working
How to fix TypeScript discriminated union errors — type guards, exhaustive checks, narrowing with in operator, never type, and common patterns for tagged unions.
Fix: CodeMirror Not Working — Editor Not Rendering, Extensions Not Loading, or React State Out of Sync
How to fix CodeMirror 6 issues — basic setup, language and theme extensions, React integration, vim mode, collaborative editing, custom keybindings, and read-only mode.
Fix: GSAP Not Working — Animations Not Playing, ScrollTrigger Not Firing, or React Cleanup Issues
How to fix GSAP animation issues — timeline and tween basics, ScrollTrigger setup, React useGSAP hook, cleanup and context, SplitText, stagger animations, and Next.js integration.