Template Literal Types
The previous chapter about Recursive Types was a good demonstration of the power of TypeScript's type system. We started applying our programming knowledge to the language of types by creating type-level algorithms. This was only the beginning! We've just started exploring the extent of what TypeScript can offer, and what's to come in upcoming chapters is even more mindblowing 🤯
In this chapter, we will learn about Template Literal Types, an awesome feature that, to my knowledge, is unique to TypeScript's type system.
I'm sure you use template literals all the time to concatenate strings together at the value level:
const firstName = "Albert";
const lastName = "Einstein";
const name = `${firstName} ${lastName}`; // <- template literal
// => "Albert Einstein"
Well, template literal types let you do the same thing with types:
type FirstName = "Albert";
type LastName = "Einstein";
type Name = `${FirstName} ${LastName}`; // <- template literal ✨type✨!
// => "Albert Einstein"
Easy, right?
More than just string interpolation
Despite their apparent simplicity, Template Literal Types open a world of possibilities. They let us build fully-typed, string-based Domain Specific Languages (DSLs) and enable some pretty cool meta-programming techniques.
A simple example is inferring the type of a DOM element based on a CSS selector:
const p = smartQuerySelector("p:First-child");
// ^? `HTMLParagraphElement | null` instead of `HTMLElement | null` 🎊
Or safely accessing a deeply nested object property using an "object path" string:
declare const obj: { some: { nested?: { property: number }[] } };
const n = get(obj, "some.nested[0].property");
// ^? `number | undefined` instead of `unknown` 🎉
💡 Press the "Edit" button at the top-right corner of any code block to see type-checking in action.
By the end of this chapter, you should not only be able to type these functions but also your very own string-based DSLs!
Let's start with the basics
As their name suggests, Template literal types are the type-level equivalents of template literals. You can use backticks (``
) to create one, and interpolate other types inside of it using the ${...}
syntax:
function smartQuerySelector<S extends string>(
selector: S
): SelectorToElement<S> | null {
return document.querySelector(selector) as any;
}
type SelectorToElement<Selector> = Get<
HTMLElementTagNameMap,
SelectorToTagName<Selector>,
null
>;
type Get<Obj, Key, Def> = Key extends keyof Obj ? Obj[Key] : Def;
type SelectorToTagName<Selector> = GetTagName<GetLastWord<Selector>>;
type GetLastWord<Str> = Last<Split<Str, " ">>;
type GetTagName<Str> = First<Split<Str, ":" | "[" | ".">>;
type Last<Tuple> = Tuple extends [...any, infer Last] ? Last : never;
type First<Tuple> = Tuple extends [infer First, ...any] ? First : never;
type Split<
Str,
Sep extends string,
Output extends string[] = [],
CurrentChunk extends string = ""
> = Str extends `${infer First}${infer Rest}`
? First extends Sep
? Split<Rest, Sep, [...Output, CurrentChunk], "">
: Split<Rest, Sep, Output, `${CurrentChunk}${First}`>
: CurrentChunk extends ""
? Output
: [...Output, CurrentChunk];
declare function get<T, S extends string>(obj: T, path: S): GetFromPath<T, S>;
type GetFromPath<Obj, Path> = RecursiveGet<Obj, ParsePath<Path>>;
type ParsePath<
Path,
Output extends string[] = [],
CurrentChunk extends string = ""
> = Path extends `${infer First}${infer Rest}`
? First extends "." | "[" | "]"
? ParsePath<
Rest,
[...Output, ...(CurrentChunk extends "" ? [] : [CurrentChunk])],
""
>
: ParsePath<Rest, Output, `${CurrentChunk}${First}`>
: [...Output, ...(CurrentChunk extends "" ? [] : [CurrentChunk])];
type RecursiveGet<Obj, PathList> = Obj extends any
? PathList extends [infer First, ...infer Rest]
? First extends keyof Obj
? RecursiveGet<Obj[First], Rest>
: [First, Obj] extends [`${number}`, any[]]
? RecursiveGet<As<Obj, any[]>[number], Rest>
: undefined
: Obj
: never;
type As<A, B> = A extends B ? A : never;