---
title: 'Template Literal Types'
source: 'https://academia.sh/en/courses/typescript/template-literal-types'
course: TypeScript
language: en
updated: '2026-08-17T18:09:53+00:00'
license: 'CC BY-SA 4.0'
---

# Template Literal Types

Expressing string formats as types, the cross product over unions, built-in string transforms, parsing a format with infer, open-ended patterns, and a literal type dissolving inside string.

The mapped types lesson used the notation `` `read${Capitalize<string & A>}` `` and
computed a field name. A **template literal type** combines string literal types to
produce new literal types.

Its notation is the same as JavaScript's template literals, except what is combined is
types, not values. This lesson covers how string formats are typed with this tool, its
behavior over unions, and its limits.

## Defining a Format

Measurement records are often referred to by a channel name: which sensor, in which
unit. This name carries a format, and the format can be typed:

```typescript
type Sensor = "temperature" | "pressure";
type Unit = "C" | "Pa";

type Channel = `${Sensor}.${Unit}`;

const channels: Channel[] = ["temperature.C", "temperature.Pa", "pressure.C", "pressure.Pa"];
console.log(channels.length, channels[0]);
```

The output is `4 temperature.C`. If the line `const wrong: Channel = "temperature.%";`
is added at the end of the file, after a blank line:

```text
q1.ts(9,7): error TS2820: Type '"temperature.%"' is not assignable to type '"pressure.C" | "pressure.Pa" | "temperature.C" | "temperature.Pa"'. Did you mean '"temperature.C"'?
```

The diagnostic writes out the computation's result explicitly: type `Channel` is a
union of four literal types. When every substituted type is a union, the result is the
**cross product** of those unions — two sensors and two units give four channels.

The suggestion at the end of the diagnostic is also a convenience the compiler offers
for literal type unions: for a value that is likely a typo, it shows the nearest valid
literal.

This type records that the string is not just any string, but carries a specific
format. It serves the same purpose as the branded type from the Structural Type
Compatibility lesson, but because the format can actually be defined, no type
assertion is needed.

## Built-in String Transforms

The compiler provides four string transforms built in: `Uppercase`, `Lowercase`,
`Capitalize`, and `Uncapitalize`.

```typescript
type Sensor = "temperature" | "pressure";

type UppercaseSensor = Uppercase<Sensor>;
type EventName = `measurement${Capitalize<Sensor>}Taken`;

const uppercase: UppercaseSensor = "TEMPERATURE";
const eventName: EventName = "measurementPressureTaken";
console.log(uppercase, eventName);
```

The output is `TEMPERATURE measurementPressureTaken`. If the line
`const wrongEvent: EventName = "measurementpressureTaken";` is added at the end of the
file, after a blank line:

```text
q3.ts(10,7): error TS2820: Type '"measurementpressureTaken"' is not assignable to type '"measurementPressureTaken" | "measurementTemperatureTaken"'. Did you mean '"measurementPressureTaken"'?
```

These transforms are built into the compiler; they cannot be written at the type level.
Case conversion follows a fixed, locale-independent rule — so locale-specific rules,
such as Turkish's distinction between dotless `ı` and dotted `i`, do not apply here.
This is one reason to stay in ASCII for channel and event names.

## Building a Key Table

A template literal type can be the key source of a mapped type. The result is a table
with a defined and complete format:

```typescript
type Sensor = "temperature" | "pressure";
type Unit = "C" | "Pa";
type Channel = `${Sensor}.${Unit}`;

type ChannelTable = {
  [K in Channel]: number;
};

const lastValues: ChannelTable = {
  "temperature.C": 21.4,
  "temperature.Pa": 0,
  "pressure.C": 0,
  "pressure.Pa": 101325,
};

console.log(lastValues["pressure.Pa"], Object.keys(lastValues).length);
```

The output is `101325 4`.

The table's keys were not counted by hand; when a third sensor is added to the `Sensor`
union, the table stays incomplete and the compiler gives a diagnostic. Expanding the
measurement model forces the data structures that use the model to be updated.

## Parsing a Format with `infer`

Template literal types can also be used on the left side of a conditional type.
`infer` extracts the pieces of the format:

```typescript
type SensorName<K> = K extends `${infer S}.${string}` ? S : never;
type UnitName<K> = K extends `${string}.${infer U}` ? U : never;

type Channel = "temperature.C" | "pressure.Pa";

declare const s: SensorName<Channel>;
declare const u: UnitName<Channel>;

const x: null = s;
const y: null = u;
```

```text
q5.ts(9,7): error TS2322: Type '"pressure" | "temperature"' is not assignable to type 'null'.
  Type '"pressure"' is not assignable to type 'null'.
q5.ts(10,7): error TS2322: Type '"C" | "Pa"' is not assignable to type 'null'.
  Type '"C"' is not assignable to type 'null'.
```

Parsing worked together with distribution: each member of the `Channel` union was
matched separately, and the results were combined into a union.

This type can be used in the signature of a run-time parse:

```typescript
type Sensor = "temperature" | "pressure";
type Unit = "C" | "Pa";
type Channel = `${Sensor}.${Unit}`;

type SensorName<K extends Channel> = K extends `${infer S}.${string}` ? S : never;

function sensorName<K extends Channel>(channel: K): SensorName<K> {
  return channel.split(".")[0] as SensorName<K>;
}

const result = sensorName("pressure.Pa");
console.log(result);
```

The output is `pressure`. If the line
`const wrongAssign: "temperature" = sensorName("pressure.Pa");` is added at the end of
the file, after a blank line:

```text
q6.ts(14,7): error TS2322: Type '"pressure"' is not assignable to type '"temperature"'.
```

The call `sensorName("pressure.Pa")` has type `"pressure"`, not `string`. The return
type was computed from the literal type given at the call site.

Notice the `as SensorName<K>` in the body: the limit established in the previous lesson
applies here too, a conditional return type does not resolve inside the body. This is a
debt that accepts the body honors the promise made in the signature, and because it is
collected in a single line, it stays testable.

## Open-Ended Patterns

The substituted type does not have to be a union; `string` can be written too. The
result is not a fixed set but a **pattern type**:

```typescript
type MeasurementEvent = `measurement:${string}`;
type WarningEvent = `warning:${string}`;
type Occurrence = MeasurementEvent | WarningEvent;

function route(occurrence: Occurrence): string {
  if (occurrence.startsWith("measurement:")) {
    return `logged: ${occurrence}`;
  }
  return `reported: ${occurrence}`;
}

console.log(route("measurement:received"));
console.log(route("warning:threshold-exceeded"));
```

Output:

```text
logged: measurement:received
reported: warning:threshold-exceeded
```

The type `` `measurement:${string}` `` covers **every** string starting with
`"measurement:"`. Because it is not a closed set, it cannot be enumerated one by one
like in the previous section; in exchange, the prefix requirement is preserved. If the
line `const wrong: MeasurementEvent = "warning:received";` is added at the end of the
file, after a blank line:

```text
u12.ts(15,7): error TS2322: Type '"warning:received"' is not assignable to type '`measurement:${string}`'.
```

This pattern is used for strings whose body is free but whose format is binding, such
as event names or key prefixes. There is no need to keep a fixed list; the compiler
reports it when the prefix is misspelled.

## A Literal Type Dissolving Inside `string`

Open-ended patterns come with a trap. A literal type **disappears** when it is put in
the same union as `string`:

```typescript
type Channel = "temperature.C" | string;

declare const channel: Channel;
const x: null = channel;
```

```text
u8.ts(4,7): error TS2322: Type 'string' is not assignable to type 'null'.
```

The diagnostic writes `string` instead of `Channel`: since the `"temperature.C"`
member is already inside the `string` set, the union has reduced to `string`. The
result is a type carrying no constraint at all — while the way it is written gives the
impression of offering a suggestion list.

This is a mistake often made when trying to model "known values plus free text." The
right tool is a closed union or an open-ended pattern, used exactly where the
constraint is actually wanted:

```typescript
type Channel = `${"temperature" | "pressure"}.${"C" | "Pa"}`;

function parseUnit(channel: Channel): string {
  return channel.split(".")[1];
}

const channels: Channel[] = ["temperature.C", "pressure.Pa"];
console.log(channels.map(parseUnit).join(","));
```

The output is `C,Pa`. If the following two lines are added at the end of the file,
after a blank line:

```typescript
const free: string = "temperature.C";
parseUnit(free);
```

```text
u13.ts(11,11): error TS2345: Argument of type 'string' is not assignable to parameter of type '"pressure.C" | "pressure.Pa" | "temperature.C" | "temperature.Pa"'.
```

The literal types have not dissolved here: the type is still a closed four-value set,
and a free string is not accepted. Adding `string` to a literal type union is exactly
the operation that removes that guarantee.

## The Union Size Limit

The cross product grows fast. Two three-member unions give nine results; every extra
component multiplies the factor. The compiler has an upper limit:

```typescript
type Letter = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j";
type Combination = `${Letter}${Letter}${Letter}${Letter}${Letter}`;
declare const x: Combination;
const y: null = x;
```

```text
q9.ts(2,20): error TS2590: Expression produces a union type that is too complex to represent.
```

Ten letters combined five at a time is a hundred thousand literal types, and the
compiler rejects it. The limit can change by version; the real lesson is that a
template literal type is suitable when the format is **closed**, and unsuitable when
it is open.

The combinatorial explosion concept from the Algorithms course shows up here in the
type checker itself. For freely formatted strings, the right tool is not a literal
type union but run-time validation and a branded type.

## Summary

- A template literal type defines string formats at the type level by combining
  literal types; when the substituted types are unions, the result is the cross
  product of those unions.
- `Uppercase`, `Lowercase`, `Capitalize`, and `Uncapitalize` are built into the
  compiler and work independently of locale.
- When a template literal type is used as a mapped type's key source, a table with a
  defined and complete format is obtained; on the left side of a conditional type,
  `infer` extracts format pieces and the result distributes over unions.
- When `string` is substituted in, the type becomes not a closed set but an
  open-ended pattern type; the prefix requirement is preserved, values cannot be
  enumerated.
- A literal type dissolves when placed in the same union as `string`, and the union
  reduces to `string`; "known values plus free text" cannot be modeled this way.
- The cross product grows fast, and past a certain size the compiler gives the
  `TS2590` diagnostic; closed literal type sets can only be built at a limited size.

## Next Step

Most of the transforms written by hand in this topic — a read-only version, an
optional version, filtering a member out of a union — already exist as ready-made
types in the standard library. The next lesson introduces these utility types, shows
which mechanism each is built on, and rewrites the measurement model's derivatives
using them.
