How to fix Template literal substitution type must be literal or union in TypeScript
This compiler error means a template literal type is trying to interpolate a placeholder whose type is too broad—usually just `string` or `any`. Template literal placeholders only resolve when they are literal string unions, so the fix is to feed the template a finite set of string literals instead of a wide type.
Quick fix
Fixes Template literal substitution type must be literal or union
type Prefixed<T extends string> = `prefix:${T}`;
type Bad = Prefixed<string>; // ❌ Template literal substitution type must be literal or union- Spot which placeholder is too wide
- Feed the template a literal union
- Turn runtime values into literal unions
- Extract literal keys from objects
type Prefixed<T extends string> = `prefix:${T}`;type Bad = Prefixed<string>; // ❌ Template literal substitution type must be literal or unionTemplate literal substitution type must be literal or union
What this error means
Template literal types concatenate string literal types at the type level. Each `${...}` placeholder must resolve to a literal or union of literals so the compiler can enumerate the final set of acceptable strings. If the placeholder is a plain `string`, `any`, or some type that widens from runtime data, the compiler cannot know which literal values will appear, so it raises "Template literal substitution type must be literal or union". The error is a safeguard that keeps your template literal types expressive but tractable.
Typical symptoms
- tsc reports "Template literal substitution type must be literal or union" on a template literal type declaration
- You are building a string via ``${...}`` inside a type and the type parameter is inferred as just `string` or `any`
- Errors disappear when you replace the placeholder with a literal union such as `'draft' | 'published'`
- IntelliSense squiggles run along the template literal definition (not necessarily the usage site)
- The problem surfaces when you derive the placeholder from `Object.keys`, runtime arrays, or untyped `Record<string, ...>` values
Common causes
- Passing a generic type parameter of `string`, `any`, or another wide primitive into `${...}`
- Collecting keys via `Object.keys` or `Record<string, ...>` without narrowing them to literal unions
- Assuming template literal types will automatically infer a union from runtime constants without `as const`
- Using template literal type helpers inside mapped types where the input is not constrained to literal strings
- Letting `const name: string` widen from a function parameter before building the template literal
How to fix "Template literal substitution type must be literal or union"
The compiler error points to the template literal that wraps the substitution. Identify the placeholder whose type is widening beyond literal values:
type Prefixed<T extends string> = `prefix:${T}`;
type Bad = Prefixed<string>; // ❌ Template literal substitution type must be literal or unionIn this example, T can be any string, so TypeScript refuses to expand the template. The fix is to ensure T represents a finite union of literal strings instead of the open-ended string type.
Replace the placeholder with a known union so the template declares exactly which strings can appear:
type Status = "/draft" | "/published";
type StatusPath<S extends Status> = `/status${S}`;
type Draft = StatusPath<"/draft">; // ✅ emits "/status/draft"As soon as S is a literal union, the error vanishes because the compiler can enumerate the resulting strings.
When you build unions from runtime data, lock them to literal types with as const or helper functions:
const events = ["created", "updated", "deleted"] as const;
type EventName = (typeof events)[number]; // "created" | "updated" | "deleted"
type Topic<E extends EventName> = `${E}-event`;as const tells TypeScript to treat each string as a literal, so events[number] stays a union instead of widening to string. Without the assertion you would still see the substitution error because the placeholder would be string again.
Measured objects like Record<string, ...> or enums default to string, so derive a literal union with keyof typeof or Extract:
const routes = {
home: "/",
dashboard: "/dashboard",
} as const;
type RouteKey = keyof typeof routes; // "home" | "dashboard"
type RoutePath<K extends RouteKey> = `${routes[K]}?section`;Now the placeholder is constrained to RouteKey, which is a literal union, and the template compiles without error.
Helpers such as Object.keys, Object.entries, and Array.prototype.map widen to string[]. Wrap them in a typed helper or as const so they return literal tuples:
function literalKeys<T extends readonly string[]>(...keys: T) {
return keys;
}
const methodNames = literalKeys("getUser", "setUser");
type MethodName = typeof methodNames[number]; // "getUser" | "setUser"
type RpcCall<M extends MethodName> = `${M}Rpc`;When the placeholder comes from MethodName, the template literal resolves cleanly.
Run the compiler and observe that the template literal error disappears:
npx tsc --noEmitIf the error is gone, IntelliSense and incremental builds will represent the literal union you intended instead of falling back to string. Keep testing with the combinations of literals you support to validate future changes.
Advanced notes
### Why the compiler insists on literal or union placeholders
Template literal types produce explicit unions of string literals. When a placeholder resolves to string, number, boolean, or bigint, TypeScript defaults the entire template to string, which defeats the purpose of capturing a finite set of outcomes. The error prevents this fallback, forcing you to model the allowed strings with literal unions or helper types.
### Const assertions, satisfies, and branded helpers
as const and the satisfies operator keep runtime arrays from widening to string[]. For example, const states = ["draft", "published"] as const satisfies readonly typeof states[number][]; gives you a literal union while still allowing the helper to validate runtime inputs.
### When you really need to accept any string
If you legitimately want a template literal to accept any string, return the primitive string instead of using a template literal type, or explicitly intersect with string:
type AnyPath = `${string}/path`; // resolves to stringBecause the placeholder is string, the compiler doesn't throw the literal-or-union error, but also doesn't preserve leftovers like section in the resulting type, so reserve this pattern for when you do not need the extra type safety.
Frequently asked questions
- What does "Template literal substitution type must be literal or union" mean?
- Template literal types concatenate string literal types at the type level. } placeholder must resolve to a literal or union of literals so the compiler can enumerate the final set of acceptable strings. If the placeholder is a plain string, any, or some type that widens from runtime data, the compiler cannot know which literal values will appear, so it raises "Template literal substitution type must be literal or union".
- How do I fix "Template literal substitution type must be literal or union"?
- This compiler error means a template literal type is trying to interpolate a placeholder whose type is too broad—usually just string or any. Template literal placeholders only resolve when they are literal string unions, so the fix is to feed the template a finite set of string literals instead of a wide type.
- What causes "Template literal substitution type must be literal or union"?
- Passing a generic type parameter of string, any, or another wide primitive into ${...} Collecting keys via Object.keys or Record<string, ...> without narrowing them to literal unions Assuming template literal types will automatically infer a union from runtime constants without as const
- Is the "Template literal substitution type must be literal or union" error serious?
- It is a low-severity error — more of a blocker than a risk. The steps above resolve it.
Official resources & further reading
- 📚TypeScript Official Documentation
- 📖TypeScript Handbook — Template Literal Types(Official Docs)
- 📖TypeScript 4.1 Release Notes — Template Literal Types(Official Docs)