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.
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.
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.
### 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.
Type parameter 'X' is not used in the function signature
How to fix "Type parameter not used in function signature" in TypeScript
Type parameter 'X' is defined but never used
How to fix "Type parameter is defined but never used" in TypeScript
Function expression requires a return type
Function expression requires a return type
Value of type 'string | undefined' is not iterable
How to fix "Value is not iterable" in TypeScript
Type 'undefined' is not assignable to type 'string'
How to fix "Type undefined is not assignable to type string" in TypeScript