Template Literal Types and String Manipulation

Learn to build and manipulate string-shaped types using template literal types and TypeScript's intrinsic string manipulation utilities: Uppercase, Lowercase, Capitalize, and Uncapitalize.

Template literal types bring JavaScript's backtick string syntax into the type system, letting you build new string literal types out of existing ones, generate matching key names, and even validate or parse the shape of a string at compile time. Paired with the built-in casing utilities, they're the foundation of type-safe event names, routes, and CSS-in-JS property names.

Template literal type syntax

A template literal type looks exactly like a JavaScript template literal, but appears in type position and can embed other types inside ${} rather than only values. type Greeting = Hello, ${string}; describes any string that starts with the literal text "Hello, " followed by any string — it's a type, not a single value, and it matches a whole family of concrete strings.

When you embed a union type inside a template literal type, TypeScript expands it into the full cross-product of possible strings. type Size = "small" | "medium" | "large"; type SizeClass = btn-${Size}; produces the union "btn-small" | "btn-medium" | "btn-large" — every combination is generated automatically, which would be tedious and error-prone to write out by hand as three separate literal types.

Multiple embedded types multiply together the same way: a template literal with two union placeholders produces every combination of both, so it's easy to accidentally generate a very large union if both placeholders have many members — something worth watching for in large codebases where compile time matters.

Generating keys and combining with mapped types

Template literal types are most powerful combined with keyof and mapped types, since a mapped type's as clause can use a template literal built from the original key K to generate a new, related key name. This is exactly the mechanism used to build a matching set of setter names, event handler names, or CSS custom property names from a base set of field names, all inside a single generic type.

The example type EventName = on${Capitalize<string>}; describes any string starting with "on" followed by a capitalized rest — useful as a loose constraint for handler prop names like onClick or onSubmit, though because string is infinite, this particular type accepts any capitalized suffix rather than a fixed, finite list; it's a shape constraint, not an enumeration.

Intrinsic string manipulation types

TypeScript ships four built-in generic types that transform string literal types at compile time: Uppercase<T>, Lowercase<T>, Capitalize<T>, and Uncapitalize<T>. Unlike ordinary generic types, these are implemented by the compiler itself rather than as library code, because they need to perform actual character-level transformation on literal type text, something normal TypeScript syntax can't express.

Uppercase<T> and Lowercase<T> convert every character of a string literal type to upper or lower case respectively, mirroring the runtime String.prototype.toUpperCase and toLowerCase. Capitalize<T> upper-cases only the first character, leaving the rest untouched, and Uncapitalize<T> lower-cases only the first character — both are the type-level equivalent of a one-line runtime string helper, but resolved entirely during compilation with no runtime cost.

These four combine naturally with template literal types and key remapping: Capitalize<string & K> is the standard idiom for turning a lowercase property name like age into Age before splicing it into a larger template literal like get${Capitalize<string & K>}, producing getAge.

Template literal inference

Just as infer can pull a type out of a matching structural shape in a conditional type, it can also pull a substring out of a matching template literal type. type ExtractId<T> = T extends user-${infer Id} ? Id : never; matches any string literal type starting with "user-" and captures whatever comes after it into Id, giving you a lightweight, compile-time string parser.

This pattern is genuinely useful for parsing structured string literal types such as route parameters (/users/${infer UserId}), CSS units (${infer Value}px), or prefixed identifiers, letting a library express 'this string must have this shape, and here's the piece I extracted from it' entirely through the type system, with autocomplete and validation for free.

Template literal types and casing utilities

Embedding a union inside a template literal type produces the full cross-product of possible string literals. Uppercase, Lowercase, Capitalize, and Uncapitalize are intrinsic types implemented by the compiler, not ordinary library types. infer works inside a template literal type's extends clause too, capturing whatever substring matches the inferred position.

type EventName = `on${Capitalize<string>}`;

type Size = "small" | "medium" | "large";
type SizeClass = `btn-${Size}`;
// "btn-small" | "btn-medium" | "btn-large"

type Loud = Uppercase<"hello">;        // "HELLO"
type Quiet = Lowercase<"HELLO">;       // "hello"
type Title = Capitalize<"hello">;      // "Hello"
type Lower = Uncapitalize<"Hello">;    // "hello"

type ExtractId<T> = T extends `user-${infer Id}` ? Id : never;
type Id1 = ExtractId<"user-42">;  // "42"

Code Examples

Capitalize<HttpMethod> capitalizes every member of the HttpMethod union, and the surrounding template literal prefixes each with "on", generating exactly the three valid handler names as a union. Anything outside that exact set, like "onPatch", is rejected at compile time. Expected result: Compile error on registerHandler("onPatch"): Argument of type '"onPatch"' is not assignable to parameter of type 'HandlerName'.

type HandlerName = on${Capitalize<HttpMethod>};

ExtractParams recursively walks a route string literal type, using infer to peel off one :param segment at a time and recursing on whatever remains, exactly like a recursive conditional type over an array — except here it's parsing structure out of a string literal type instead of an array type.

T extends ${string}:${infer Param}/${infer Rest} ? Param | ExtractParams</${Rest}> : ...

Common mistake: Forgetting the second, no-trailing-slash branch (matching just ${string}:${infer Param} with nothing after) means the last parameter in a route, which has no following slash, never gets captured.

type HttpMethod = "get" | "post" | "delete";

type HandlerName = `on${Capitalize<HttpMethod>}`;
// "onGet" | "onPost" | "onDelete"

function registerHandler(name: HandlerName) {
  console.log(`Registering ${name}`);
}

registerHandler("onPost");   // OK
registerHandler("onPatch");  // Error: not assignable to HandlerName
type Route = "/users/:id" | "/posts/:postId/comments/:commentId";

type ExtractParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractParams<`/${Rest}`>
    : T extends `${string}:${infer Param}`
      ? Param
      : never;

type Params1 = ExtractParams<"/users/:id">;
// "id"

type Params2 = ExtractParams<"/posts/:postId/comments/:commentId">;
// "postId" | "commentId"

Template literal type expansion over a union

A union embedded inside a template literal type expands into the cross-product of every possible resulting string, generated automatically rather than listed by hand.

type Size = "small" | "medium" | "large"
         |
  `btn-${Size}`
         |
   "btn-small" | "btn-medium" | "btn-large"

type HandlerName = `on${Capitalize<HttpMethod>}`
   HttpMethod: "get" | "post"
         |  Capitalize each member
         v
   "Get" | "Post"
         |  prefix with "on"
         v
   "onGet" | "onPost"

Important Rules

A template literal type describes a family of matching string literal types, not a single string, and is written with the same backtick and ${} syntax as a runtime template literal.

Embedding a union type inside ${} produces the cross-product of every combination, which can grow quickly with multiple placeholders.

Uppercase, Lowercase, Capitalize, and Uncapitalize are compiler-intrinsic types for transforming string literal types; they have no user-space equivalent because they manipulate literal type text directly.

infer works inside a template literal type's extends clause to capture a matching substring, enabling compile-time parsing of structured string literal types.

Where this shows up

Typed routing libraries use template literal type inference to extract path parameter names directly from a route string literal like /users/:id, so the params object passed to a handler is automatically and correctly typed with no manually maintained parameter list. CSS-in-JS and design-system libraries use template literal types to constrain custom property names (like --color-${string}) and generate matching TypeScript types for spacing scales, breakpoints, and color tokens directly from a single source array, keeping the type system and the actual generated CSS from drifting apart.

type Loud = Uppercase<string>; // expecting a literal transformation type Loud = Uppercase<"hello">; // "HELLO"

Uppercase applied to the general string type just returns string, since there's no specific literal text to transform; it only produces a useful, narrowed literal result when given an actual string literal type as input.

type Combo = ${Size}${Color}; // assuming this stays a small union

// Be deliberate: this produces Size's member count times Color's member count total strings

Multiple union placeholders in one template literal type multiply together; two unions of 5 members each produce 25 resulting string literal types, which is easy to underestimate and can slow compilation if pushed too far.

[predict-output] What does type SizeClass = \btn-${"sm" | "lg"}\; evaluate to? A. "btn-sm" | "btn-lg" B. "btn-sm-lg" C. string D. A compile error

A. "btn-sm" | "btn-lg" — Embedding a union inside a template literal type expands into the cross-product of possible strings, here producing the union "btn-sm" | "btn-lg".

[multiple-choice] What is the difference between Capitalize<T> and Uppercase<T>? A. They are identical B. Capitalize upper-cases only the first character; Uppercase upper-cases the entire string C. Uppercase only works on the first character; Capitalize works on the whole string D. Capitalize is a runtime function, Uppercase is a type

B. Capitalize upper-cases only the first character; Uppercase upper-cases the entire string — Capitalize<T> transforms only the first character of the string literal type to upper case, leaving the rest unchanged, while Uppercase<T> converts every character to upper case.

[multiple-choice] In type ExtractId<T> = T extends \user-${infer Id}\ ? Id : never;, what does infer Id do? A. Declares a runtime variable named Id B. Captures whatever substring follows "user-" in a matching string literal type C. Forces T to be exactly "user-" D. Converts T to lowercase

B. Captures whatever substring follows "user-" in a matching string literal type — infer inside a template literal type's extends clause pattern-matches the literal structure and captures the matching portion (here, everything after the literal "user-" prefix) into the new type variable Id.

Write a template literal type CssVariable that matches any string starting with "--" followed by any string (e.g. "--primary-color"), and a type Getter<T> that, given a union of lowercase field names, produces a union of matching getter names prefixed with "get" and capitalized, e.g. "age" becomes "getAge".

type CssVariable = --${string}; type Getter<T extends string> = get${Capitalize<T>}; type Result = Getter<"age" | "name">; // "getAge" | "getName"

CssVariable needs --${string}. Getter needs get${Capitalize<T>} where T is constrained to extend string.