Skip to content

Fix: TypeScript Template Literal Type Error — Type Not Assignable or Inference Fails

FixDevs ·

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 Size

Why 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.
  • string in 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 union

Fix 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 SpacingScale

Still 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' — literal

For related TypeScript issues, see Fix: TypeScript Mapped Type Error and Fix: TypeScript Generic Constraint Error.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles