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}`
\text{Colors} = \{\text{``black''}, \text{``white''}, \text{``purple''}\}
\text{Animals} = \{\text{``goose''}, \text{``hamster''}\}
A\times B=\{(a,b)\mid a\in A\ {\text{ and }}\ b\in B\}

Multiple interpolated positions

Cartesian Product

\text{Colors} = \{\text{``black''}, \text{``white''}, \text{``purple''}\}
\text{Animals} = \{\text{``goose''}, \text{``hamster''}\}
A\times B=\{(a,b)\mid a\in A\ {\text{ and }}\ b\in B\}
Color/Animal goose hamster
black
white
purple

Multiple interpolated positions

\text{Colors} = \{\text{``black''}, \text{``white''}, \text{``purple''}\}
\text{Animals} = \{\text{``goose''}, \text{``hamster''}\}
A\times B=\{(a,b)\mid a\in A\ {\text{ and }}\ b\in B\}
Color/Animal goose hamster
black (black, goose) (black, hamster)
white
purple

Multiple interpolated positions

\text{Colors} = \{\text{``black''}, \text{``white''}, \text{``purple''}\}
\text{Animals} = \{\text{``goose''}, \text{``hamster''}\}
A\times B=\{(a,b)\mid a\in A\ {\text{ and }}\ b\in B\}
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

References

THANK YOU