Template Literal Types
Sixian Li
Quick Refresher
Type is a set of values
Type
Type | Values | Size |
---|---|---|
boolean | true, false | 2 |
string | "a", "b", "Ollie"... | ∞ |
"foo" | "foo" | 1 |
string[] | ["1"], ["2", "Kickflip"] | ∞ |
"foo" | "bar" | "foo", "bar" | 2 |
never | (no value, similar to ∅) | 0 |
extends
A extends B
⇨ A is assignable to B
⇨ Every value in the set A is assignable to a variable of type B
⇨ B can be replaced with any value in the set A without a problem
Conditional Types
When the type on the left of the extends is assignable to the one on the right, then you’ll get the “true” branch; otherwise you’ll get the “false” branch.
SomeType extends OtherType ? TrueType : FalseType;
interface Animal {
sleep(): void;
}
interface Dog extends Animal {
woof(): void;
}
type Example1 = Dog extends Animal ? number : string; // number
type Example2 = RegExp extends Animal ? number : string; // string
Template Literal Types
Syntax
JavaScript template literal string
const a = `string text ${expression} string text`
TypeScript template literal types
type World = "world";
type Greeting = `hello ${World}`;
= "hello world"
Union
When a union is used in the interpolated position, the result is distributed.
"You fill the gap with each of the value."
type World = "world" | "tomorrow";
type Greeting = `hello ${World}`;
= "hello world" | "hello tomorrow"
TemplateLiteral<A | B | C> =
TemplateLiteral<A> | TemplateLiteral<B> | TemplateLiteral<C>
type Colors = "black" | "white" | "purple"
type Animals = "goose" | "hamster"
type ColoredAnimals = `${Colors} ${Animals}`
Multiple interpolated positions
Cartesian Product
Color/Animal | goose | hamster |
---|---|---|
black | ||
white | ||
purple |
Multiple interpolated positions
Color/Animal | goose | hamster |
---|---|---|
black | (black, goose) | (black, hamster) |
white | ||
purple |
Multiple interpolated positions
Color/Animal | goose | hamster |
---|---|---|
black | (black, goose) | (black, hamster) |
white | (white, goose) | (white, hamster) |
purple | (purple, goose) | (purple, hamster) |
Multiple interpolated positions
Color/Animal | goose | hamster |
---|---|---|
black | (black, goose) | (black, hamster) |
white | (white, goose) | (white, hamster) |
purple | (purple, goose) | (purple, hamster) |
type Colors = "black" | "white" | "purple"
type Animals = "goose" | "hamster"
type ColoredAnimals = `${Colors} ${Animals}`
= "black goose" | "black hamster" | "white goose" | "white hamster" | "purple goose" | "purple hamster"
Multiple interpolated positions
Destructuring
`infer`
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
type EverythingBeforeWorld<T> = T extends `${infer H}World` ? H : never;
type T1 = EverythingBeforeWorld<"Hello World">;
= "Hello "
Inferred type variables may be referenced in the true branch of the conditional type
Match on the structure of the string
Examples
1. logEngagement
1. logEngagement
We want to ensure there is no "." in area, scenario and operation strings
1. logEngagement
function logEngagement<TScenario extends string, TOperation extends string>(
area: EngagementLogArea,
scenario: NoDotString<TScenario>,
operation: NoDotString<TOperation>
): void {
console.log(`${prefix}.${area}.${scenario}.${operation}`)
}
type NoDotString<S extends string> = S extends `${any}.${any}` ? never : S
2. Smart Split
type TopicSource = "User" | "VivaTopics" | "Yammer";
// Response from API
// "User,VivaTopics" | "User,Yammer" | "VivaTopics,User" | ...
2. Smart Split
type TopicSource = "User" | "VivaTopics" | "Yammer";
// Response from API
// "User,VivaTopics" | "User,Yammer" | "VivaTopics,User" | ...
type Res = `${TopicSource},${TopicSource}`;
2. Smart Split
type TopicSource = "User" | "VivaTopics" | "Yammer";
// Response from API
// "User,VivaTopics" | "User,Yammer" | "VivaTopics,User" | ...
type Res = `${TopicSource},${TopicSource}`;
declare const res: Res;
declare let source: TopicSource;
// Q1: What is the type of a?
const a = res.split(",");
// Q2: Does this work?
source = a[0];
type TopicSource = "User" | "VivaTopics" | "Yammer";
// e.g., 'User,VivaTopics' | 'VivaTopics,Yammer'
type Res = `${TopicSource},${TopicSource}`;
declare const res: Res;
// Q: What is the type of a?
// A: string[]
const a = res.split(",");
type TopicSource = "User" | "VivaTopics" | "Yammer";
// Response from API
// "User,VivaTopics" | "User,Yammer" | "VivaTopics,User" | ...
type Res = `${TopicSource},${TopicSource}`;
declare const res: Res;
declare let source: TopicSource;
// Q1: What is the type of a?
// A1: string[]
const a = res.split(",");
// Q2: Does this work?
source = a[0];
2. Smart Split
type TopicSource = "User" | "VivaTopics" | "Yammer";
// e.g., 'User,VivaTopics' | 'VivaTopics,Yammer'
type Res = `${TopicSource},${TopicSource}`;
declare const res: Res;
// Q: What is the type of a?
// A: string[]
const a = res.split(",");
type TopicSource = "User" | "VivaTopics" | "Yammer";
type Res = `${TopicSource},${TopicSource}`;
declare const res: Res;
declare let source: TopicSource;
// Q1: What is the type of a?
// A1: string[]
const a = res.split(",");
// Q2: Does this work?
// A2: No
source = a[0];
2. Smart Split
split(separator: string | RegExp, limit?: number): string[];
⬆️ ☹️
type TopicSource = "User" | "VivaTopics" | "Yammer";
type Res = `${TopicSource},${TopicSource}`;
declare const res: Res;
declare let source: TopicSource;
// Q1: What is the type of a?
// A1: string[]
const a = res.split(",");
// Q2: Does this work?
// A2: No
source = a[0];
2. Smart Split
2. Smart Split
interface String {
smartSplit<TString extends string, TSep extends string>(
this: TString,
sep: TSep
): Split<TString, TSep>;
}
type Split<
TString extends string,
TSep extends string
> = TString extends `${infer L}${TSep}${infer R}`
? [L, ...Split<R, TSep>]
: [TString];
type SplitTailRecursive<
TString extends string,
TSep extends string,
Acc extends string[] = []
> = TString extends `${infer L}${TSep}${infer R}`
? SplitTailRecursive<R, TSep, [...Acc, L]>
: Acc;
2. Smart Split
3. querySelector
document.querySelector('div#app') // ==> HTMLDivElement
document.querySelector('div#app > form#login') // ==> HTMLFormElement
document.querySelectorAll('span.badge') // ==> NodeListOf<HTMLSpanElement>
anElement.querySelector('button#submit') // ==> HTMLButtonElement
3. querySelector
// lib.dom.d.ts
/** Returns the first element that is a descendant of node that matches selectors. */
querySelector<K extends keyof HTMLElementTagNameMap>(selectors: K): HTMLElementTagNameMap[K] | null;
querySelector<K extends keyof SVGElementTagNameMap>(selectors: K): SVGElementTagNameMap[K] | null;
querySelector<E extends Element = Element>(selectors: string): E | null;
const a = document.querySelector('div') // HTMLDivElement
const b = document.querySelector('div > button') // Element
const c = document.querySelector(' ') // Element, but run-time error
3. querySelector
type WhiteSpace = " " | "\n" | "\t" | "\r" | "\f";
type Trim<S extends string> = S extends `${WhiteSpace}${infer Rest}`
? Trim<Rest>
: S extends `${infer Rest}${WhiteSpace}`
? Trim<Rest>
: S;
type Combinator = " " | "+" | "~" | ">";
type GetLastElementTag<TSelector> =
TSelector extends `${infer _}${Combinator}${infer Rest}`
? GetLastElementTag<Rest>
: TSelector;
type T1 = GetLastElementTag<'div > button'> // ==> "button"
type IsInvalid<TSelector extends string> = Trim<TSelector> extends '' | `${any}${Combinator}` ? true : false
type Invalid1 = IsInvalid<'div'> // ==> false
type Invalid2 = IsInvalid<'div >'> // ==> true
type TagNameToElement<TTagName> = TTagName extends keyof HTMLElementTagNameMap
? HTMLElementTagNameMap[TTagName]
: never;
type QuerySelector<TSelector extends string> = IsInvalid<TSelector> extends true ? never : TagNameToElement<GetLastElementTag<TSelector>>
type Q1 = QuerySelector<'div > button ~ li'> // ==> HTMLLIElement