🌱BeginnerAdvanced Beginner Types

Mapped & Conditional Types

Create new types by transforming existing ones mapped types, conditional types, template literal types, and infer.

Overview

TypeScript lets you compute types from other types programmatically. **Mapped types** iterate over the keys of an existing type and transform each property. This is how Partial, Readonly, Required, and Pick are implemented internally: `{ [K in keyof T]: T[K] }` is a mapped type that creates an exact copy of T. **Conditional types** use a ternary syntax at the type level: `T extends U ? X : Y`. They answer "if T is assignable to U, the type is X; otherwise Y". **Template literal types** let you create string union types by combining string literals with type variables ```type EventName = `on${string}````. **`infer`** lets you extract a sub-type inside a conditional type the foundation of `ReturnType` and `Parameters`.

Key Points

  • •Mapped type: `{ [K in keyof T]: NewType }` iterate and transform
  • •Add `?` in mapped type: `{ [K in keyof T]?: T[K] }` = Partial; `-?` removes optional
  • •Add `readonly`: `{ readonly [K in keyof T]: T[K] }` = Readonly
  • •Conditional type: `T extends U ? X : Y` type-level ternary
  • •`infer R` capture a sub-type inside a conditional: used in ReturnType<F>
  • •Template literal types: `type Route = `/${string}`` pattern-constrained strings
  • •Distributive: conditional types distribute over union types

Examples

Mapped types building custom utilities

// Building your own Partial
type MyPartial<T> = { [K in keyof T]?: T[K] };

// Building your own Readonly
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };

// Transform values   make all values string
type Stringify<T> = { [K in keyof T]: string };

interface User { id: number; name: string; active: boolean }

type PartialUser  = MyPartial<User>;    // { id?: number; name?: string; active?: boolean }
type ReadonlyUser = MyReadonly<User>;   // { readonly id: number; ... }
type StringUser   = Stringify<User>;    // { id: string; name: string; active: string }

// Map only specific keys
type NullableFields<T, K extends keyof T> = Omit<T, K> & {
  [P in K]: T[P] | null;
};
type NullableEmail = NullableFields<User, "name">;

Conditional types

// IsString true if T is a string
type IsString<T> = T extends string ? true : false;

type A = IsString<string>;   // true
type B = IsString<number>;   // false
type C = IsString<string | number>; // boolean (distributed)

// NonNullable remove null and undefined
type MyNonNullable<T> = T extends null | undefined ? never : T;

type D = MyNonNullable<string | null | undefined>;  // string

// FlattenArray unwrap array type
type FlattenArray<T> = T extends Array<infer U> ? U : T;

type E = FlattenArray<string[]>;  // string
type F = FlattenArray<number[]>;  // number
type G = FlattenArray<boolean>;   // boolean (not an array, returns T)

// ReturnType  built from conditional + infer
type MyReturnType<F extends (...args: any[]) => any> =
  F extends (...args: any[]) => infer R ? R : never;

function getUser(id: number) { return { id, name: "Alice" }; }
type UserResult = MyReturnType<typeof getUser>; // { id: number; name: string }

Template literal types

// Event handler names
type EventName = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"

// CSS property names
type CssMargin = `margin${"-top" | "-right" | "-bottom" | "-left" | ""}`;
// "margin" | "margin-top" | "margin-right" | ...

// API route pattern
type ApiRoute = `/api/${string}`;
function callApi(route: ApiRoute, method: "GET" | "POST"): void {
  console.log(`${method} ${route}`);
}
callApi("/api/users", "GET");
// callApi("/admin", "GET");  // Error: doesn't match /api/...

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

interface Person { name: string; age: number }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }

Common Mistakes

  • Applying conditional types to a union without realising they distribute `string | number extends string` evaluates each member separately
  • Using `infer` outside conditional types `infer` only works inside `extends` clauses of conditional types
  • Over-engineering with mapped/conditional types when utility types suffice use Partial, Pick, etc. before building custom ones

Note

Mapped and conditional types look scary at first. Start by reading them like sentences: `T extends string ? "yes" : "no"` = "if T is a string, the answer is yes; otherwise no". Once you see them as type-level if statements and loops, they become natural.

Exercises

Write TypeScript code to solve each exercise. Click Run to compile and execute it right here, then mark as done.

Loading editor…

Completed: 0 / 5 exercises

Knowledge Check

Question 1 of 4

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