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 inkeyword in the index signature for mapping over the keys.
- Note the use of the askeyword in the index signature for renaming the keys.
- Note the indexed access type for typing argin 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↗.