TypeScript · Lesson 7 of 12

Mapped, Conditional and Template Literal Types

Master advanced TypeScript types: keyof, indexed access, mapped types with key remapping, conditional types with infer, and template literal types.

  • Advanced
  • 18 min read
  • 4 objectives

Before this lessonLesson 6: Classes, Access Modifiers and Abstract Classes

What you will learn

  • Use keyof and indexed access types
  • Build mapped types and remap keys with as
  • Write conditional types and extract parts with infer
  • Generate string types with template literals

Your Progress

0 of 12 lessons 0%

  • Lessons0 / 12
  • Completed0
  • Est. time left~ 3 hours

Create a free account to keep your progress on every device.

Tip: pressing Next marks this lesson complete automatically.

Utility types like Partial and Pick feel like magic until you learn they are written in ordinary TypeScript. This lesson opens the box. You will learn the handful of type-level tools behind them, so you can read library types and, occasionally, write your own.

A fair warning up front: these tools are for keeping types in sync automatically, typically in shared libraries and API layers. In application code, a plain interface is usually clearer.

keyof and indexed access

keyof T produces a union of an object type's keys. T[K] (an indexed access type) looks up the type of a property, just like obj[key] looks up a value. Together they let one type follow another.

interface User {
  id: number;
  name: string;
  role: "admin" | "member";
}

type UserKey = keyof User;          // "id" | "name" | "role"
type Role = User["role"];            // "admin" | "member"
type IdOrName = User["id" | "name"]; // number | string

// Works on arrays too: number indexes every element
const PLANS = ["free", "pro", "team"] as const;
type Plan = (typeof PLANS)[number];  // "free" | "pro" | "team"

Mapped types

A mapped type loops over a union of keys and builds a new object type. The syntax { [K in Keys]: Type } reads like a for loop at the type level. Here is how Partial and Readonly are actually defined:

type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };

// Modifiers can also be removed with a minus sign
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type Concrete<T> = { [K in keyof T]-?: T[K] };   // same as Required<T>

// Map every field to a different type
type Validators<T> = { [K in keyof T]: (value: T[K]) => boolean };

const userValidators: Validators<User> = {
  id: (v) => v > 0,                       // v: number
  name: (v) => v.length > 0,              // v: string
  role: (v) => v === "admin" || v === "member",
};

The Validators example is the practical payoff: add a field to User and the compiler immediately demands a validator for it.

Key remapping with as

Inside a mapped type, as lets you rename keys or filter them out (mapping a key to never drops it). Combined with template literal types (below), this generates things like getter names.

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string; getRole: () => "admin" | "member" }

// Keep only keys whose value is a string
type StringKeys<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};
type UserStrings = StringKeys<User>;   // { name: string; role: "admin" | "member" }

Conditional types

A conditional type is a ternary for types: A extends B ? X : Y. If A is assignable to B, the result is X, otherwise Y. When A is a bare type parameter and you pass a union, the condition runs on each member separately (this is called distribution), which is how Exclude works.

type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<"hi">;   // "yes"
type B = IsString<42>;     // "no"

// Distribution over unions: built-in Exclude and Extract
type MyExclude<T, U> = T extends U ? never : T;
type NonAdmin = MyExclude<"admin" | "member" | "guest", "admin">;  // "member" | "guest"

type MyNonNullable<T> = T extends null | undefined ? never : T;
type Clean = MyNonNullable<string | null | undefined>;             // string

Extracting types with infer

Inside the extends clause, infer X declares a type variable that TypeScript fills in by pattern matching. This is how ReturnType and Awaited pull pieces out of other types.

type MyReturnType<F> = F extends (...args: any[]) => infer R ? R : never;
type ElementOf<T> = T extends readonly (infer E)[] ? E : never;
type Unwrap<T> = T extends Promise<infer V> ? Unwrap<V> : T;   // recursive, like Awaited

async function fetchOrders() {
  return [{ id: "A1", total: 49 }];
}

type Orders = Unwrap<MyReturnType<typeof fetchOrders>>;  // { id: string; total: number }[]
type Order = ElementOf<Orders>;                          // { id: string; total: number }

That last pair is a real pattern: derive the type from the function that produces the data, so the type can never drift out of date.

Template literal types

Template literal types use backtick syntax at the type level to build string types. Given unions, they produce every combination. TypeScript also ships Uppercase, Lowercase, Capitalize and Uncapitalize helpers.

type Size = "sm" | "md" | "lg";
type Tone = "primary" | "danger";
type ButtonClass = `btn-${Tone}-${Size}`;
// "btn-primary-sm" | "btn-primary-md" | ... 6 combinations

type Entity = "user" | "order";
type EventName = `${Entity}:${"created" | "deleted"}`;
// "user:created" | "user:deleted" | "order:created" | "order:deleted"

function on(event: EventName, handler: () => void) { /* ... */ }
on("order:created", () => {});
// on("order:updated", () => {});   // Error: not assignable to type EventName

// Pattern matching on strings with infer
type RouteParam<S> = S extends `${string}:${infer P}/${infer Rest}`
  ? P | RouteParam<`/${Rest}`>
  : S extends `${string}:${infer P}` ? P : never;
type Params = RouteParam<"/users/:userId/orders/:orderId">;  // "userId" | "orderId"

The RouteParam type is roughly how routers like Express types and React Router extract :params from a path string, so req.params.userId is known to exist.

Types exist only at compile time, but it helps to see the runtime equivalent of what ButtonClass enumerates:

const tones = ["primary", "danger"];
const sizes = ["sm", "md", "lg"];
const classes = tones.flatMap((t) => sizes.map((s) => `btn-${t}-${s}`));
console.log(classes.length);
console.log(classes.join(" "));
Output
6
btn-primary-sm btn-primary-md btn-primary-lg btn-danger-sm btn-danger-md btn-danger-lg

Recap

  • keyof T gives a union of keys; T[K] gives the type of a property.
  • Mapped types { [K in keyof T]: ... } transform every property; as renames or filters keys.
  • Conditional types A extends B ? X : Y distribute over unions; wrap in [ ] to stop that.
  • infer extracts parts of a type, which is how ReturnType and Awaited work.
  • Template literal types build and pattern-match string types such as event names and route params.
// Write your solution here

Finished reading? Mark this lesson complete to track your progress.

Up next · Lesson 8Enums, Modules and Declaration FilesUnderstand TypeScript enums vs as const, ES module imports and type-only imports, and how .d.ts declaration files and @types packages type JavaScript.