webdev.complete
🧠 Advanced TypeScript
🛡️TypeScript
Lesson 45 of 117
30 min

Mapped & Conditional Types

The TS type system as a programming language.

Once you understand mapped types, conditional types, and template literal types, the rest of TypeScript's utility library stops feeling magical. They're the three primitives from which every advanced trick (including the built-in utility types you saw earlier) is built. Let's open the hood.

Mapped types: walking the keys of T

A mapped type says "for every key in this type, produce a new key with this value." Think of it as a .map() for types.

ts
// The canonical form
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

// Or always-readonly:
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

// Or strip both modifiers:
type Unfreeze<T> = {
  -readonly [K in keyof T]-?: T[K];
};
// The leading minus removes the modifier instead of adding it

Built-in Partial, Readonly, and Required are literally one-line mapped types. Once you see this, you can build your own custom versions in seconds.

Transforming the value type

The right-hand side can be anything. You can wrap, unwrap, or completely replace the original value type.

ts
// Stringify every value
type Stringify<T> = {
  [K in keyof T]: string;
};

// Wrap every value in a Promise
type Async<T> = {
  [K in keyof T]: Promise<T[K]>;
};

// A &quot;changed?&quot; flag for every key
type Dirty<T> = {
  [K in keyof T]: boolean;
};

Key remapping with `as`

TS 4.1 added the asclause inside a mapped type. It lets you rename keys on the fly. This is how you build a "getters" type from a regular object type.

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

type User = { name: string; age: number };
type UserGetters = Getters<User>;
// {
//   getName: () => string;
//   getAge: () => number;
// }
Filter keys by remapping to never
If you remap a key to never, it gets dropped. This is the standard trick for filtering keys:
ts
type OmitByValue<T, V> = {
  [K in keyof T as T[K] extends V ? never : K]: T[K];
};
type WithoutBooleans = OmitByValue<User, boolean>;

Conditional types: a ternary for types

A conditional type is T extends U ? X : Y: "if T is assignable to U, the type is X, otherwise Y." This is how you make a type depend on another type.

ts
type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<"hello">;  // "yes"
type B = IsString<42>;        // "no"

// Useful real-world example: only require an argument if needed
type NotificationFn<T> = T extends void
  ? () => void
  : (payload: T) => void;

Distributive conditional types

When you write T extends U ? X : Y and Tis a naked type parameter that resolves to a union, the conditional distributes over each member of the union.

ts
type ToArray<T> = T extends any ? T[] : never;

type Many = ToArray<string | number>;
// = ToArray<string> | ToArray<number>
// = string[] | number[]

// To prevent distribution, wrap in a tuple
type ToArrayNoDistrib<T> = [T] extends [any] ? T[] : never;
type One = ToArrayNoDistrib<string | number>;
// (string | number)[]

infer: extracting a type from inside another

The infer keyword introduces a new type variable inside the extends clause. The compiler figures out what it must be. This is how ReturnType and friends are implemented.

ts
// Pull the return type out of any function signature
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type R1 = MyReturnType<() => string>;             // string
type R2 = MyReturnType<(n: number) => number[]>;  // number[]

// Pull the element type out of an array
type ElementOf<T> = T extends (infer E)[] ? E : never;
type X = ElementOf<number[]>;                      // number
type Y = ElementOf<{ name: string }[]>;            // { name: string }

// Pull the value type out of a Promise
type Unpromise<T> = T extends Promise<infer V> ? V : T;
type P = Unpromise<Promise<string>>;                // string

Template literal types

TS 4.1 also introduced template literal types. You can build new string-literal types by interpolating other string literals.

ts
type Greeting = `hello, ${string}`;
type G1 = Greeting;  // matches "hello, Ada", "hello, anyone"

// Combine with unions to enumerate
type Lang = "en" | "fr" | "ja";
type Region = "US" | "UK" | "JP";
type Locale = `${Lang}-${Region}`;
// "en-US" | "en-UK" | "en-JP" | "fr-US" | ... (9 strings)

// Built-in helpers
type Up = Uppercase<"hello">;        // "HELLO"
type Low = Lowercase<"HELLO">;       // "hello"
type Cap = Capitalize<"hello">;      // "Hello"
type Un = Uncapitalize<"Hello">;     // "hello"

Putting it together: a typed route param parser

Let's build something real. Given a route string like"/users/:id/posts/:postId", derive an object type with the right param names.

ts
type ExtractParams<S extends string> =
  S extends `${string}:${infer Param}/${infer Rest}`
    ? { [K in Param | keyof ExtractParams<`/${Rest}`>]: string }
    : S extends `${string}:${infer Param}`
      ? { [K in Param]: string }
      : {};

type R1 = ExtractParams<"/users/:id">;
// { id: string }

type R2 = ExtractParams<"/users/:id/posts/:postId">;
// { id: string; postId: string }

function buildPath<S extends string>(template: S, params: ExtractParams<S>): string {
  return template; // implementation elided
}

buildPath("/users/:id/posts/:postId", { id: "1", postId: "42" });  // OK
buildPath("/users/:id/posts/:postId", { id: "1" });
// ^ Error: missing 'postId'
Recursive types
That ExtractParamscalls itself. Recursive conditional types (TS 4.1+) are how you walk arbitrarily deep strings, arrays, or trees. There's a recursion limit (~1000 instantiations) but it's usually plenty.

Building DeepReadonly

The built-in Readonlyonly freezes the top level. Here's how you write a deep version. Notice how it combines mapped types with a conditional and recursion:

ts
type DeepReadonly<T> = T extends object
  ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : T;

type State = {
  user: { name: string; address: { city: string } };
  tags: string[];
};

type Frozen = DeepReadonly<State>;
// readonly all the way down

The lookup operator T[K]

Combine mapped types with indexed access (T[K]) and you can extract precisely the type of a property. This is what makes utilities like Pick work:

ts
type User = { id: string; age: number };

type IdType = User["id"];          // string
type Values = User[keyof User];     // string | number

// A type-safe getter using a generic key
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const u = { id: "u_1", age: 36 };
const id = get(u, "id");   // id is string
const age = get(u, "age"); // age is number

A custom utility: PartialBy

A practical demonstration. Make only specific keys optional, leave the rest required. This is genuinely useful for API responses where most fields are required but a few might be omitted.

ts
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

type User = { id: string; name: string; email: string };

type DraftUser = PartialBy<User, "id">;
// { name: string; email: string; id?: string }

const draft: DraftUser = { name: "Ada", email: "ada@example.com" };  // OK
Quiz1 / 4

What does the mapped type `{ [K in keyof T]: boolean }` produce?

Recap

  • A mapped type walks keyof T and produces a new object type. Add ?, readonly, or their minus variants to flip modifiers.
  • as in a mapped type lets you rename or filter keys (remap to never to drop).
  • Conditional types: T extends U ? X : Y. With a naked union, they distribute over each member.
  • infer introduces a type variable the compiler unifies for you. Use it to extract pieces of function types, arrays, promises.
  • Template literal types let you build and pattern-match strings. Combined with infer, you get tools like a typed route param parser.
  • The built-in utility types are all one-line combinations of these primitives.
Built with Next.js, Tailwind & Sandpack.
Learn. Build. Ship.