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↗.