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✨!
// ^?
type FirstName = "Albert";
type LastName = "Einstein";
type Name = `${FirstName} ${LastName}`; // <- template literal ✨type✨!
// ^?
Easy, right?
More than just string interpolation
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;
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");
// ^? instead of `HTMLElement | null` 🎊
const p = smartQuerySelector("p:First-child");
// ^? 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");
// ^? instead of `unknown` 🎉
declare const obj: { some: { nested?: { property: number }[] } };
const n = get(obj, "some.nested[0].property");
// ^? instead of `unknown` 🎉
💡 This code block is editable. Try updating `obj` 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:
Enroll in Type-Level TypeScript!
Get access to all chapters of the Type-Level TypeScript course and join a community of 1600+ students!
You will find everything you need to become a TypeScript Expert — 11 chapters of in-depth, unique content, and more than 70 fun challenges to practice your new skills.
Full access to all 11 chapters
70 type challenges with explanations
Lifetime access to all course materials
Exclusive discord community
Loading...