I need to learn about TypeScript Template Literal Types
Tags: typescript, strings, parsing, types
Published: 2020-09-27 01:54:21

It's Sunday in New Zealand and I don't want to get out of bed yet, so instead I'm going to listen to the [new Menzingers album](https://open.spotify.com/album/4NA26Wy1s046SpVs6VYij9) and learn about [TypeScript Template Literal Types](https://devblogs.microsoft.com/typescript/announcing-typescript-4-1-beta/#template-literal-types) and write down what I found out as I go!

## TypeScript string types:

Let's start with what I already know.

* TypeScript has a `string` type. It covers all strings like `const hello = "Hello World";`, or ``const myName = `My name is ${name}`;``.
* You can also use a string literal type, such as `type Hello = 'hello'`, which only matches that specific string.
* You can use [Union Types](https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html#union-types) to be combine string literal types to be more precise about allowed string inputs. One good example is `type Events = 'click' | 'doubleclick' | 'mousedown' | 'mouseup' | ...;`

There are limitations to what TypeScript can know. Template strings will cause specific string types to expand out to the generic `string` type:

```typescript
type A = 'a';
const a: A = `${'a'}`; // Argument of type 'string' is not assignable to parameter of type '"a"'.
```

In my experience, once you start typing stuff with specific strings you often end up duplicating a bunch of stuff too. Take the `Events` example from before:

```typescript
type EventNames = 'click' | 'doubleclick' | 'mousedown' | 'mouseup';
type Element = {
    onClick(e: Event): void;
    onDoubleclick(e: Event): void;
    onMousedown(e: Event): void;
    onMouseup(e: Event): void;
    addEventListener(eventName: Event): void;
};
```

If I add a new event name to the `EventNames` type, I also have to change the `Element` type! That's probably fine most of the time, but it *could* cause issues.

## Template Literal Types "basics"

###### (Spoiler: it's not basic at all!)

The [PR where Template Literal Types](https://github.com/microsoft/TypeScript/pull/40336) looked cool when I first read it, and people got pretty excited! Then the [TypeScript 4.1 Beta release notes](https://devblogs.microsoft.com/typescript/announcing-typescript-4-1-beta/#template-literal-types) came out, so I'm going to go through that first.

TypeScript 4.1 can concatenate strings in types using the same syntax from JavaScript:

```typescript
type World = "world";

type Greeting = `hello ${World}`;
// same as
//   type Greeting = "hello world";
```

Using Union Types with concatenation enables combinations:

```typescript
type VerticalAlignment = "top" | "middle" | "bottom";
type HorizontalAlignment = "left" | "center" | "right";
type Alignment = `${VerticalAlignment}-${HorizontalAlignment}`

declare function setAlignment(value: Alignment): void;

setAlignment("top-left"); // works!
setAlignment("middle-right"); // works!
setAlignment("top-middel"); // error!
```

There's also some fancy new mapping syntax which means I can change the `Element` type from before:

```typescript
type EventNames = 'click' | 'doubleclick' | 'mousedown' | 'mouseup';
type Element = {
  [K in EventNames as `on${Capitalize<EventNames>}`]: (event: Event) => void;
} & {
  addEventListener(eventName: EventNames): void;
};
// same as 
// type Element = {
//  onClick(e: Event): void;
//  onDoubleclick(e: Event): void;
//  onMousedown(e: Event): void;
//  onMouseup(e: Event): void;
//  addEventListener(eventName: Event): void;
//};
```

That's pretty deep - it takes each of the strings in the `EventNames` type, passing it to a `Capitalize` type, and prepending `on` to each of them! Now if I add a new event name to the `EventNames`, the `Element` type will already reflect it!

These new features are obviously really powerful, and people have been making some amazing stuff, e.g.:

{% twitter 1301976526603206657 %}

{% twitter 1304515347932999680 %}

[Grégory Houllier](https://twitter.com/ghoullier) collected some of these [examples](https://github.com/ghoullier/awesome-template-literal-types) into one place, so I can see how they work by looking at the implementations!

### Type-safe string dot notation:

{% twitter 1309489079378219009 %}

[Full implementation here.](https://www.typescriptlang.org/play?ts=4.1.0-pr-40336-88#code/FAFwngDgpgBACgQxACwJIFsIBsA8AVAGhgGkowYoAPEKAOwBMBnGAazIHsAzGPAPhgC8wGCTIVqdJjEYgATgEtaAc2EwA-DwDapMAF1xNBswBKUAMbtZ9HDIXKiCWmF6qRGgD4wABgBIA3joAvgB0-ogoGNj42mS6RACilGZYAK70UDhsYFxaOnGsHNyOYJq6vPwAZNJyikqBXq4iMJ6+AWQh-onJaRlZOXgxekR9RU6lldV2dQ1NMABcMLRQAG5QsqoLS6uyANzAoJCw4WiYWABM+PwC8EgnUYQF2dx8zY-9ewfQNyiXgt9351+VEMUlstVeIx46n+kUBLwWkLwH3AX2OADUEKkMg84AZJMxjpcrqpccD8d5-IpOGtRGAOn4qTTTDJ6qoNDo8UY3s9GhpmSBOVJCQM8i5Zm5-hisdE8kR+WLxZsVmtGkrtht4ILmIjeVo4LpVYtlbt9ulkghZLBOClaGYQPJ2LQYEooCB8ERSRIucLygAKdgAIwAVgsHhBbgs4ABKSO3KUpbEe3gfCy0GQwQNB8wC65+VSceSyGQAOQQ6CgCwARAAReRQJTsSsEVRYBAlssVmCVgASCAAXk3VAgXQsAMwABmbIggsnYWbtjAWmkafkWHarpgQLHkICbMFTNQDKRAlkXMAAbAAOGCBKdNVe0dddgDCjpkSCge4PCiPJ6LCwARjOG87wNQIYDbfc3xAD4XRAf1g2zIhKxnOds0YYJx2Cb95F-U9KyjHYgA) 

#### What does it do?

```
const user = {
  projects: [
    { name: "Cool project!", contributors: 10 },
    { name: "Amazing project!", contributors: 12 },
  ]
};

get(user, "projects.0.contributors"); // <- I want this string to be type-safe!
```

I thought I was starting with an easy one, but it's still pretty complex! I simplified it a little bit (and probably broke it) but it'll be easier to figure out - my implementation is [here](https://www.typescriptlang.org/play?ts=4.1.0-pr-40336-88&ssl=30&ssc=12&pln=30&pc=23#code/C4TwDgpgBACghsAFgHgCoBooGkIihAD2AgDsATAZygGtcB7AMylSgF4b6nUA+NgKChQAFDjyFi5KhWAAnAJYkA5gKgB+ZgG1RAXXxFSlKACUIAYzoyyyafKWY4JENxWD1AHygADACQBvUQC+AHR+8EhoWrjamACiBKYANgCuZBDItCCMmjqYGVkAgjIycCDIDk7cvABkUDYKigGeLoJQHj7+uMF+cYkpaXlckSDRHJlMhcWl5ZVQNXVKjc2CHqLNAFxQJBAAbhAyKhtbuzIAlHx8oJCwCIgAanDJaRiwehKGYSg8vKwqMK8GVHaCgYe2wnRCvmBoJM0kWrjBYn0klGWVQzXUMOA-2RHwiOmcLRa6g+90eeKimExBMJUEOOz26029P2gg2f3EAJRXHRmhg2kZRz2AG5zqlEnAZNAGEkSKZgHI6CQoIoIMA0Jh2Uj3jc0JUhHQAEYAKw2zzANzZJzZN1JSSeGu4Ir45hI0igSQooPYvhUYBkdCNZmAFA2Gmavk2cAAthANgAiADCdDoCSgfoDQYAhHHMC7ZHIDUlgBYQ1AAIwABigAXQ4cjMfj+SjcAAXvU0-7A3Ls7nFfnC8WZKWywAmau1wT8gJQOBUF3SJ0q4BCD17TAAcnTXeDQQr65OIqAA).

#### How does it work?

I'll look at `PathValue` first.

``` typescript
type PathValue<T, P extends Path<T>> =
  P extends `${infer Key}.${infer Rest}`
  ? Key extends keyof T
    ? Rest extends Path<T[Key]>
      ? PathValue<T[Key], Rest>
      : never
    : never
  : P extends keyof T
    ? T[P]
    : never;
```

This is the code that take an object and a valid path to an object and returns the type of the value at the end of that path.

Conditional types are really hard to process, so I'm going to rewrite it how I think about it.

`PathValue` is a generic type so it's kind of like a type function, and it takes two things, `T` which could be anything, and `P` which has to be a valid `Path` for `T`. `PathValue` is also a conditional type - it has the shape `A extends B ? C : D`. In this case it has several *nested* conditionals! But each of the `never` bits is a condition that doesn't return a type, so I can simplify it down to the two valid condition paths. That looks something like this:

```typescript
typefunction PathValue (T, P: Path<T>) {
  if (P extends `${infer Key}.${infer Rest}` && Key extends keyof T && Rest extends Path<T[Key]>) {
    return PathValue<T[Key], Rest>;
  }
  if (P extends keyof T) {
    return T[P];
  }
}
```

Since the first condition actually calls `PathValue` again, this is a *recursive conditional type* 🤯🤯🤯. There are two base conditionals, one continues the recursion, the other ends it. Again I'll look at the "easier" one first.

```typescript
if (P extends keyof T) {
  return T[P];
}
```

If `P` is just a string and it is an exact key of `T`, then return the type of that key. That means it is the end of the path and it can stop recursing.

The other condition is the magical bit.

```typescript
if (P extends `${infer Key}.${infer Rest}` && Key extends keyof T && Rest extends Path<T[Key]>) {
  return PathValue<T[Key], Rest>;
}
```

Here's the fancy new bit:

```
P extends `${infer Key}.${infer Rest}`
```

This type says "check if the string contains a `'.'`, and give me the two string literal types either side of the `'.'`". The equivalent JavaScript would be something like:

```typescript
const [Key, Rest] = P.split('.');
```

The next part of the conditional takes the first string literal (`Key`) and makes sure it is a valid key of T:

```typescript
Key extends keyof T
```

The last part of the conditional takes the second string literal (`Rest`) and makes that it is a valid `Path` for the type of `T[Key]`.

So in the case of the example:

```typescript
const user = {
  projects: [
    { name: "Cool project!", contributors: 10 },
    { name: "Amazing project!", contributors: 12 },
  ]
};
get(user, "projects.0.contributors");
```

If these conditions are all true, then the recursion continues and you go to the level in the object, and the next chunk of the dot-notation string.

That kind of makes sense and I now kind of understand ``P extends `${infer Key}.${infer Rest}` `` which seems pretty important. Next up is the `Path` type:

```typescript
type Path<T, Key extends keyof T = keyof T> =
  Key extends string
  ? T[Key] extends Record<string, any>
    ? | `${Key}.${Path<T[Key], Exclude<keyof T[Key], keyof Array<any>>> & string}`
      | `${Key}.${Exclude<keyof T[Key], keyof Array<any>> & string}`
      | Key
    : never
  : never;
```

Again I'm going to write it out in a different way:

```typescript
typefunction Path<T, Key extends keyof T = keyof T> {
  if (Key extends string && T[Key] extends Record<string, any>) {
    return `${Key}.${Path<T[Key], Exclude<keyof T[Key], keyof Array<any>>> & string}` | `${Key}.${Exclude<keyof T[Key], keyof Array<any>> & string}` | Key;
  }
}
```

This says that it `Key` is a string, and the type of the property on type `T` (Aka `T[Key]`) is a `Record`, then return some fancy union. There are three parts to the union:

```typescript
`${Key}.${Path<T[Key], Exclude<keyof T[Key], keyof Array<any>>> & string}`

`${Key}.${Exclude<keyof T[Key], keyof Array<any>> & string}`

Key;
```

What does the `Exclude<keyof T[Key], keyof Array<any>>` bit mean? It uses TypeScript's built-in `Exclude` type which will remove any types in the second parameter from the first. In this specific case, it is going to remove any valid key for an Array (e.g. `push`, `map`, `slice`). I guess this also includes Object keys, but I'm not super sure how that works off the top of my head. This bit seems to me to be a bit of a nice to have, as it reduces the final set of possible paths a bit, but I can ignore it for now. That gives me:

```typescript
`${Key}.${Path<T[Key], keyof T[Key]> & string}`

`${Key}.${keyof T[Key] & string}`

Key;
```

The `& string` bit is a little trick to reduce `keyof T[Key]` down to only being a `string` - I think because you can have symbol keys as well. So I can ignore that too:

So the final union is basically:

```typescript
`${Key}.${Path<T[Key], keyof T[Key]>}` | `${Key}.${keyof T[Key]}` | Key;
```

This is another recursive type, where each level of recursion is concatenating the valid key paths like `` `${Key}.{Path}` ``, so you get `` `${Key}.{Path}` |  ${Key}.{(`${Key}.{Path})`} | `${Key}.{(`${Key}.{Path})`}` `` ... etc. That handles all the deeply nested keys. That is combined with the very next layer of keys `${Key}.${keyof T[Key]}`, and the current keys `Key`.

So at a high level there are two recursive types, one with recurses through the valid keys of an object and builds up the whole valid set, using Template Literal Types to concatenate the keys with a `"."`. The other type splits the concatenated keys and works out the type at each layer of the path. Makes sense I think? Pretty powerful stuff if you hide it away behind a nice API in a library.

### Type-safe document.querySelector:

{% twitter 1308472279010025477 %}

[Full implementation here.](https://www.typescriptlang.org/play?ts=4.1.0-dev.20200920#code/C4TwDgpgBAymA2BLYAeGUIA9gQHYBMBnKQ4AJ0VwHMAaKAEQ2zyJPMqoD4oBeWJnAWIADACQBvSgDMIZKABUAvhPrLJuGXICqi4VAD8UANry6AOgtwkqLXXqcAulABcxmA4DcAKFCQFAQwBrCAAZf1IUADVuPkiBFmIjJ0NcCAA3WRcoOKxBViNSCmpk7KMABidXHOYhY0KOcwtpTIAlEvkg0PDUFu5XVIyyb19oeQoAWxCIKVRqvOJ66hjs+Nq9CWa5Ft0DBQmpmZRerMjh8FGJlsQqAAtZ1dZFrl4V3ISoMXVNKG2P3bHEOMrrcen1smc-ADxlEHgt2EsXlCDqgocC7lFOJwIdAYOwwABZAD2+EQUkQshhb1qTzo+NhbCKz1i9M+mygITU+LUbIAgjtDCETtjYHiiSSybJCJSao94UyRRQCcTSeSyGhRcqJWrcYqxSqKTrEErxaqonQAORmc2cC0AYmtFqMDqg5uc1uFHWCYVIPJmsnkhOCuGl8wZDQU9Key0Nxv1ZClnq6ESsyBQUIxdHkmKxPnOUAA4hBgN7gABReAQcZ4YAAOX8VZD7yjiM6Jd9ODIAaDadb3Xb-sDeDNLpdNtH7tzfkLZf8AGMbuXK9W6w3InRBVTZYyki8kssvFBD68ZYknAejxeBVBzxfD1V6QU5Wfb0fDEYLGYQnRpyXF1XcLW9YQFE5QOI414vnex6ho+jKNGYbJtBBkGGNOpZzguFb-oBDYtHQ74WF+BZFr+WHLkBIEVI43A3rerhJMKaFkQBK4QFKcxNnKyxoRhf7kQ2KazBaNDWjmIxQHxAEAEIgKxjbUlxLy0RxtTBCAhJSFAAAS8j4iEknAB0VCsfi-hgMhL6GDpekGUZJlmUYkTPpB96bsQakabAkT5rZ-jGUBpnmbRl5eT5zGGX59lgI5zkvq4BnCqZwDzuhKXhcOG4nhJ4VQAAPlAuAAK7wPALxFSV+4Xip+SxbeV7BUerlZbBHC1SFgr5QZMlyZEoE0ZBJwPjSUAfohbWvlASUpbx6V4eyeXZUu0myRRvVUf1Lnst4k7QAAioVsggC0bHFSikaKXwU0LjNS2oExt2sQmmLbQA9AAVG9B5vRJmD1ggEBfS9Xj4BAs7wP4ZDQFIhW4LOwCIISuBQAAjgdZAgDAEAVnDhJqvI52MpwAAUqOHa48gAJSuPth3HYQp1pjmXizojpBQP4Lyk+jmPY8AuNE+aJJpGYABG-i4KkcjcP4Zizv4JUALR8wrc7w4j5oU1AL0vQr3DWSEPKwzcuMGczrPAFAIuc2jGNY6DfNkALlBgIVwB0ELGtazreu6SEACSuAu2WOX5fr9CIGkpss7gbOztbh08-b-PmrOiBkGDEBGLOmA8AARAAjAArGUucOJ72u66FADCacZ1H5tQPg8fc3bOOO+aIuu3zuC2p3IAK7ghIAO7l972m+1JXeI-XMcW9AfBc7bvPJ4Q9sI0jYDOGS8bAArGlK+cGseF7lf6wAChDflkGZmG3V4QA) 

#### What does it do?

This one is a little different, as it doesn't validate that the string is a valid CSS selector (although I'm pretty sure that would be possible with these new types), but it does figure out the best type of the result of the query:

```
const a = querySelector('div.banner > a.call-to-action') //-> HTMLAnchorElement
const b = querySelector('input, div') //-> HTMLInputElement | HTMLDivElement
const c = querySelector('circle[cx="150"]') //-> SVGCircleElement
const d = querySelector('button#buy-now') //-> HTMLButtonElement
const e = querySelector('section p:first-of-type'); //-> HTMLParagraphElement
```

#### How does it work?

Let's look at some of the helper types first:

```typescript
type Split<S extends string, D extends string> = S extends `${infer T}${D}${infer U}` ? [T, ...Split<U, D>] : [S];
type TakeLast<V> = V extends [] ? never : V extends [string] ? V[0] : V extends [string, ...infer R] ? TakeLast<R> : never;
type TrimLeft<V extends string> = V extends ` ${infer R}` ? TrimLeft<R> : V;
type TrimRight<V extends string> = V extends `${infer R} ` ? TrimRight<R> : V;
type Trim<V extends string> = TrimLeft<TrimRight<V>>;
```

These are *super* clever, and seem like they could live alongside `Capitalize` etc in the base TypeScript types.

##### Split:

```typescript
type Split<S extends string, D extends string> = S extends `${infer T}${D}${infer U}` ? [T, ...Split<U, D>] : [S];
```

Again I'm going to rewrite it:

```typescript
typefunction Split<S extends string, D extends string> {
    if (S extends `${infer T}${D}${infer U}`) {
        return [T, ...Split<U, D>];
    }
    return [S];
}
```

So there is another recursive type that takes an input string, and some splitter string `D`. If the input string contains the splitter string, the part of the string that comes before the splitter is put into an array, and then the second part of the string is passed to the `Split` type again. The result is splatted (`...`) which means that the final result will be a single flattened array of strings.

If the input string doesn't contain the splitter, then the whole string is returned. It's wrapped in an array so that the splat works.

##### TakeLast:

```typescript
type TakeLast<V> = V extends [] ? never : V extends [string] ? V[0] : V extends [string, ...infer R] ? TakeLast<R> : never;
```

This one doesn't have anything to do with Template Types particularly but it's still interesting. Rewriting gives me something like this:

```typescript
typefunction TakeLast<V> {
    if (V extends []) {
        return;
    }
    if (V extends [string]) {
        return V[0];
    }
    if (V extends [string, ...infer R]) {
        return TakeLast<R>;
    }
}
```

One change I might make to this would be to have `type TakeLast<V>` be `typefunction TakeLast<V extends Array<string>>`? That would limit the valid input types and possibly give an easier error message.

Three different paths through here:

1) If the array is empty, return nothing.
2) If the array contains one element, return it.
3) If the array contains more than one element, skip the first element and call `TakeLast` on the array of remaining elements.

##### TrimLeft/TrimRight/Trim:

```
type TrimLeft<V extends string> = V extends ` ${infer R}` ? TrimLeft<R> : V;
type TrimRight<V extends string> = V extends `${infer R} ` ? TrimRight<R> : V;
type Trim<V extends string> = TrimLeft<TrimRight<V>>;
```

More Template String types here:

`Trim` is pretty nice, it just calls `TrimRight` and then `TrimLeft`.

`TrimLeft` and `TrimRight` are basically the same so I'll just rewrite one of them:

```typescript
typefunction TrimLeft<V extends string> {
    if (V extends ` ${infer R}`) {
        return TrimLeft<R>;
    }
    return V;
}
```

And I'll actually rewrite this again cause what it's actually doing is:

```typescript
typefunction TrimLeft<V extends string> {
    if (V.startsWith(' ')) {
        return TrimLeft<R>;
    }
    return V;
}
```

This type recurses until it finds a string that doesn't start with a space. Makes sense, but still very cool to see it as a type.

`TrimRight` is pretty much identical but it does an `endsWith` instead.

##### StripModifiers

The last bit of Template Type magic I want to look at here is:

```
type StripModifier<V extends string, M extends string> = V extends `${infer L}${M}${infer A}` ? L : V;
type StripModifiers<V extends string> = StripModifier<StripModifier<StripModifier<StripModifier<V, '.'>, '#'>, '['>, ':'>;
```

That can be rewritten to be something like this:

```typescript
typefunction StripModifier<V extends string, M extends string> {
    if (V.contains(M)) {
        const [left, right] = V.split(M);
        return left;
    }
    return V;
}
```

Then the `StripModifiers` type just uses the `StripModifier` type with each of the characters than can follow an element tag name in CSS:

```typescript
typefunction StripModifiers<V extends string> {
    StripModifier(V, '.');
    StripModifier(V, '#');
    StripModifier(V, '[');
    StripModifier(V, ':');
}
```

The rest of this example uses these different types to split the CSS selector on relevant characters (' ', '>', and ','), and then select the relevant bit of the remaining selector and returning the correct type.

A lot of the heavy lifting is done by this type:

```typescript
type ElementByName<V extends string> = 
    V extends keyof HTMLElementTagNameMap 
        ? HTMLElementTagNameMap[V] 
        : V extends keyof SVGElementTagNameMap 
        ? SVGElementTagNameMap[V] 
        : Element;
```

It maps from a string (such as 'a') to a type (such as `HTMLAnchorElement`), then checks SVG elements, before falling back to the default `Element` type.

### What next?

The next examples get progressively more bonkers, so I'm not going to write down all my thinking about them - you should check them out though and see if you can see how they work. The [JSON parser](https://github.com/jamiebuilds/json-parser-in-typescript-very-bad-idea-please-dont-use/blob/main/json-parser-in-typescript-very-bad-idea-please-dont-use.ts) is probably the best mix of complex and readable.

From this I have a couple thoughts:

1) I should definitely use this for [`TSQuery`](https://github.com/phenomnomnominal/tsquery)
2) TypeScript is probably going to need new syntax for types soon because stuff like:

```typescript
type ParseJsonObject<State extends string, Memo extends Record<string, any> = {}> =
  string extends State
    ? ParserError<"ParseJsonObject got generic string type">
    : EatWhitespace<State> extends `}${infer State}`
      ? [Memo, State]
      : EatWhitespace<State> extends `"${infer Key}"${infer State}`
        ? EatWhitespace<State> extends `:${infer State}`
          ? ParseJsonValue<State> extends [infer Value, `${infer State}`]
            ? EatWhitespace<State> extends `,${infer State}`
              ? ParseJsonObject<State, AddKeyValue<Memo, Key, Value>>
              : EatWhitespace<State> extends `}${infer State}`
                ? [AddKeyValue<Memo, Key, Value>, State]
                : ParserError<`ParseJsonObject received unexpected token: ${State}`>
            : ParserError<`ParseJsonValue returned unexpected value for: ${State}`>
          : ParserError<`ParseJsonObject received unexpected token: ${State}`>
        : ParserError<`ParseJsonObject received unexpected token: ${State}`>
``` 

is pretty tricky 😅.

All in all that was pretty useful for me, I think I get how Template Literal Types work a bit now. I guess I'll see next time I try to use them.

Let me know if this was useful, it was a pretty unfiltered and unedited 🙃