Fix: TypeScript Template Literal Type Error — Type Not Assignable or Inference Fails
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:
- 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.
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 string ✓Fix 3: Fix Inference in Generic Functions
Template literal inference requires the type parameter to be constrained correctly:
// 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 ✓
});
// Alternative — separate the event name resolution
type ChangeEvent<T, K extends keyof T> = K extends string ? `${K}Changed` : never;
function on<T, K extends keyof T>(
obj: T,
event: ChangeEvent<T, K>,
callback: (value: T[K]) => void
): void { }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"
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:
// 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 SpacingScaleStill Not Working?
Template literal types with string are too broad — \prefix-${string}`accepts any string starting with "prefix-". If you need to narrow it, replacestringwith 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' — literalFor related TypeScript issues, see Fix: TypeScript Mapped Type Error and Fix: TypeScript Generic Constraint Error.
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.