- Use template literal types to model structured subsets of
string
types and domain-specific languages (DSLs). - Combine template literal types with mapped and conditional types to capture nuanced relationships between types.
- Take care to avoid crossing the line into inaccurate types. Strive for uses of template literal types that improve developer experience without requiring knowledge of fancy language features.
////## Code Samples
type MedalColor = 'gold' | 'silver' | 'bronze';
type PseudoString = `pseudo${string}`;
const science: PseudoString = 'pseudoscience'; // ok
const alias: PseudoString = 'pseudonym'; // ok
const physics: PseudoString = 'physics';
// ~~~~~~~ Type '"physics"' is not assignable to type '`pseudo${string}`'.
interface Checkbox {
id: string;
checked: boolean;
[key: `data-${string}`]: unknown;
}
const check1: Checkbox = {
id: 'subscribe',
checked: true,
value: 'yes',
// ~~~~ Object literal may only specify known properties,
// and 'value' does not exist in type 'Checkbox'.
'data-listIds': 'all-the-lists', // ok
};
const check2: Checkbox = {
id: 'subscribe',
checked: true,
listIds: 'all-the-lists',
// ~~~~~~ Object literal may only specify known properties,
// and 'listIds' does not exist in type 'Checkbox'
};
interface Checkbox {
id: string;
checked: boolean;
[key: string]: unknown;
}
const check1: Checkbox = {
id: 'subscribe',
checked: true,
value: 'yes', // permitted
'data-listIds': 'all-the-lists',
};
const check2: Checkbox = {
id: 'subscribe',
checked: true,
listIds: 'all-the-lists' // also permitted, matches index type
};
const img = document.querySelector('img');
// ^? const img: HTMLImageElement | null
const img = document.querySelector('img#spectacular-sunset');
// ^? const img: Element | null
img?.src
// ~~~ Property 'src' does not exist on type 'Element'.
interface HTMLElementTagNameMap {
"a": HTMLAnchorElement;
"abbr": HTMLElement;
"address": HTMLElement;
"area": HTMLAreaElement;
// ... many more ...
"video": HTMLVideoElement;
"wbr": HTMLElement;
}
interface ParentNode extends Node {
// ...
querySelector<E extends Element = Element>(selectors: string): E | null;
// ...
}
type HTMLTag = keyof HTMLElementTagNameMap;
declare global {
interface ParentNode {
querySelector<
TagName extends HTMLTag
>(
selector: `${TagName}#${string}`
): HTMLElementTagNameMap[TagName] | null;
}
}
const img = document.querySelector('img#spectacular-sunset');
// ^? const img: HTMLImageElement | null
img?.src // ok
const img = document.querySelector('div#container img');
// ^? const img: HTMLDivElement | null
type CSSSpecialChars = ' ' | '>' | '+' | '~' | '||' | ',';
type HTMLTag = keyof HTMLElementTagNameMap;
declare global {
interface ParentNode {
// escape hatch
querySelector(
selector: `${HTMLTag}#${string}${CSSSpecialChars}${string}`
): Element | null;
// same as before
querySelector<
TagName extends HTMLTag
>(
selector: `${TagName}#${string}`
): HTMLElementTagNameMap[TagName] | null;
}
}
const img = document.querySelector('img#spectacular-sunset');
// ^? const img: HTMLImageElement | null
const img2 = document.querySelector('div#container img');
// ^? const img2: Element | null
// e.g. foo_bar -> fooBar
function camelCase(term: string) {
return term.replace(/_([a-z])/g, m => m[1].toUpperCase());
}
// (return type to be filled in shortly)
function objectToCamel<T extends object>(obj: T) {
const out: any = {};
for (const [k, v] of Object.entries(obj)) {
out[camelCase(k)] = v;
}
return out;
}
const snake = {foo_bar: 12};
// ^? const snake: { foo_bar: number; }
const camel = objectToCamel(snake);
// camel's value at runtime is {fooBar: 12};
// we'd like the type to be {fooBar: number}
const val = camel.fooBar; // we'd like this to have a number type
const val2 = camel.foo_bar; // we'd like this to be an error
type ToCamelOnce<S extends string> =
S extends `${infer Head}_${infer Tail}`
? `${Head}${Capitalize<Tail>}`
: S;
type T = ToCamelOnce<'foo_bar'>; // type is "fooBar"
type ToCamel<S extends string> =
S extends `${infer Head}_${infer Tail}`
? `${Head}${Capitalize<ToCamel<Tail>>}`
: S;
type T0 = ToCamel<'foo'>; // type is "foo"
type T1 = ToCamel<'foo_bar'>; // type is "fooBar"
type T2 = ToCamel<'foo_bar_baz'>; // type is "fooBarBaz"
type ObjectToCamel<T extends object> = {
[K in keyof T as ToCamel<K & string>]: T[K]
};
function objectToCamel<T extends object>(obj: T): ObjectToCamel<T> {
// ... as before ...
}
const snake = {foo_bar: 12};
// ^? const snake: { foo_bar: number; }
const camel = objectToCamel(snake);
// ^? const camel: ObjectToCamel<{ foo_bar: number; }>
// (equivalent to { fooBar: number; })
const val = camel.fooBar;
// ^? const val: number
const val2 = camel.foo_bar;
// ~~~~~~~ Property 'foo_bar' does not exist on type
// '{ fooBar: number; }'. Did you mean 'fooBar'?