Skip to content
Nate
Stephens

Template Literal Types

We can use the exact same syntax that one would find in a JavaScript template literal to create a new type that represents every possible combination of two union types of string literals..but in a type expression instead of a value expression

Basic Examples

Specify requirements for a string, such as starting with a # for a hex color value:

type HexColor = `#${string}`;

Could then verify that type either using something like zod or a custom type guard:

const isHexColor = (s: string): s is HexColor => {
  return s.startsWith('#');
};

Can include types other than string:

type RGBString = `rgb(${number} ${number} ${number})`;

Create a union of all string combinations for action types:

type ColorFormats = 'rgb' | 'hex' | 'hsl';
type ActionTypes = `update-${ColorFormats}-color`;
// type ActionTypes = "update-rgb-color" | "update-hex-color" | "update-hsl-color"

Can use more than one union of string literals:

type ArtFeatures = 'tree' | 'sunset';
type Colors = 'darkSienna' | 'sapGreen' | 'titaniumWhite';

type ArtMethodNames = `paint_${Colors}_${ArtFeatures}`;
// type ArtMethodNames =
//   | 'paint_darkSienna_tree'
//   | 'paint_darkSienna_sunset'
//   | 'paint_sapGreen_tree'
//   | 'paint_sapGreen_sunset'
//   | 'paint_titaniumWhite_tree'
//   | 'paint_titaniumWhite_sunset';

Utility Types

TypeScript provides a few utility types you can use within these template literal types:

  • UpperCase
  • LowerCase
  • Capitalize
  • Uncapitalize
type ArtMethodNames = `paint${Capitalize<Colors>}${Capitalize<ArtFeatures>}`;

// type ArtMethodNames =
//   | 'paintDarkSiennaTree'
//   | 'paintDarkSiennaSunset'
//   | 'paintSapGreenTree'
//   | 'paintSapGreenSunset'
//   | 'paintTitaniumWhiteTree'
//   | 'paintTitaniumWhiteSunset';

Key Mapping

The resultant mapped type has different property names (keys) than the type being iterated over during the mapping.

A frequent use-case is analogous to services that provide a basic CRUD API based on data-layer (or database) schemas.

By that I mean if you created a User type (or schema) the service would then create an API with naming such as createUser, updateUser, and deleteUser.

With data layer code, where often there are defined types available, you potentially have a lot of is*, get* and set* methods based on that data's property keys.

Real-World Example:

  • Note the use of the in keyword in the index signature for mapping over the keys.
  • Note the use of the as keyword in the index signature for renaming the keys.
  • Note the indexed access type for typing arg in each method using the key.
interface DataState {
  digits: number[];
  names: string[];
  flags: Record<'darkMode' | 'mobile', boolean>;
}
// Record equates to:
// type DataState['flags`] = {
//   darkMode: boolean;
//   mobile: boolean;
// }

// Create a custom type using mapped types and template literal types
// Here we're creating a type to represent `setter` methods of all the `DataState` properties
type DataSDK = {
  // The mapped type
  [K in keyof DataState as `set${Capitalize<K>}`]: (arg: DataState[K]) => void;
};

// Using the new type
function load(dataSDK: DataSDK) {
  dataSDK.setDigits([14]);
  dataSDK.setNames(['Joe', 'Jane']);
  dataSDK.setFlags({ darkMode: true, mobile: false });
}

From the Intermediate TypeScript course on FEM taught by Mike North.

From the React and TypeScript, v2 course on FEM taught by Steve Kinney.


Last Updated: