Programming Language

Advanced TypeScript Types

Utility types, conditional types and mapped types let you transform and compose types with precision. Here's how they work and when to reach for them.

intermediate16 min readUpdated Sep 15, 2026
typescript
type Users = {
  ada: { age: 36 };
  bob: { age: 28 };
};

type UserNames = keyof Users;
type Ages = Users[UserNames];
Utility types
Built-in type transforms (Partial, Pick, Record)
Conditional types
T extends U ? X : Y
Mapped types
Transform properties of an existing type
Template literals
String manipulation at the type level
infer keyword
Extract types from within other types
Distributive conditionals
Apply conditions to each union member

Why it matters

Why advanced types matter

Type transformation

Build new types from existing ones without duplicating definitions. Change, filter and reshape types to match your needs.

Precise contracts

Model complex data with exact types that catch errors others miss. Discriminated unions, exact types and branded types.

Less repetition

Derive types automatically instead of maintaining parallel definitions. One source of truth, many derived types.

The big picture

The three pillars of type transformation

Utility types, conditional types and mapped types are the tools that let you build any type from existing ones without repeating yourself.

Utility types

Built-in transforms

Partial, Required, Pick, Omit, Record, Extract, Exclude and other standard types that modify existing types.

Conditional types

Type-level logic

If/else at the type level. T extends U ? X : Y lets you branch types based on conditions.

Mapped types

Property transformation

Iterate over the keys of a type and transform each property. The foundation of most utility types.

Advanced types at a glance

What you can build

Partial<T>

Makes all properties optional.

Required<T>

Makes all properties required.

Pick<T, K>

Extracts a subset of properties.

Omit<T, K>

Removes specific properties.

Record<K, V>

Creates an object type with keys K and values V.

Extract<U>

Pulls out members of a union that match a type.

A short history

From simple aliases to type-level programming

  1. 2016

    Utility types arrive

    TypeScript 2.1 introduces mapped types and built-in utilities like Partial, Readonly and Pick.

    16
  2. 2018

    Conditional types land

    TypeScript 2.8 adds conditional types, enabling type-level logic and the infer keyword.

    18
  3. 2019

    Recursive types

    TypeScript 4.1 enables recursive conditional types and template literal types.

    19
  4. 2021

    Template literal types

    String manipulation at the type level opens new possibilities for API design.

    21
  5. 2023

    Satisfies operator

    TypeScript 4.9 adds satisfies for validating values without losing literal types.

    23
  6. Today

    Type-level programming

    Advanced types are essential for libraries, API clients and complex application state.

    Today

The complete guide

Advanced TypeScript Types: Everything you need to know

What are advanced TypeScript types?

Advanced types are the tools that let you transform, compose and manipulate types at the type level. Instead of manually defining parallel types for every variation, you derive them automatically from existing ones.

If basics give you types and generics let you write reusable code, advanced types let you build a type system that describes your data precisely — catching errors that simpler approaches miss.

The three pillars are utility types (built-in transforms), conditional types (type-level logic) and mapped types (property transformation). Together they form a small language for working with types.

Utility types

TypeScript ships with built-in types that modify other types:

Partial and Required

interface User {
  id: number;
  name: string;
  email: string;
}

// All properties optional
type UserUpdate = Partial<User>;
// { id?: number; name?: string; email?: string; }

// All properties required
type StrictUser = Required<Partial<User>>;
// { id: number; name: string; email: string; }

Pick and Omit

// Extract specific properties
type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string; }

// Remove specific properties
type UserWithoutEmail = Omit<User, "email">;
// { id: number; name: string; }

Record

// Create object type with specific keys and values
type Scores = Record<string, number>;
// { [key: string]: number }

const scores: Scores = { math: 95, science: 87 };

// With union keys
type StatusMap = Record<"idle" | "loading" | "done", string>;

Extract and Exclude

type Status = "idle" | "loading" | "success" | "error";

// Keep only matching members
type ActiveStatus = Extract<Status, "loading" | "success">;
// "loading" | "success"

// Remove matching members
type InactiveStatus = Exclude<Status, "loading" | "success">;
// "idle" | "error"

ReturnType and Parameters

function createUser(name: string, age: number) {
  return { name, age, id: Date.now() };
}

type User = ReturnType<typeof createUser>;
// { name: string; age: number; id: number }

type Args = Parameters<typeof createUser>;
// [string, number]

NonNullable

type Maybe = string | null | undefined;
type Definite = NonNullable<Maybe>;
// string

Mapped types

Mapped types iterate over the keys of an existing type and transform each property:

type ReadOnly<T> = {
  readonly [K in keyof T]: T[K];
};

type MutableUser = ReadOnly<User>;
// All properties are readonly

Adding modifiers

// Add optional
type Optional<T> = {
  [K in keyof T]?: T[K];
};

// Remove optional
type Concrete<T> = {
  [K in keyof T]-?: T[K];
};

// Remove readonly
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

The - and + operators remove or add modifiers during mapping.

Key remapping with as

TypeScript 4.1+ lets you remap keys during mapping:

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

type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string; getEmail: () => string; }

Conditional types

Conditional types express logic at the type level:

type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">; // true
type B = IsString<42>;      // false

The infer keyword

infer extracts a type from within another type:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type Fn = () => string;
type Result = ReturnType<Fn>; // string

Practical examples

// Extract array element type
type ElementOf<T> = T extends (infer E)[] ? E : never;
type Num = ElementOf<number[]>; // number

// Unwrap promises
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type Deep = Awaited<Promise<Promise<string>>>; // string

// Extract object property types
type ValuesOf<T> = T[keyof T];
type UserValues = ValuesOf<User>; // number | string

Distributive conditional types

When a conditional type distributes over a union, each member is checked separately:

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

type Result = ToArray<string | number>;
// string[] | number[] — not (string | number)[]

This is useful for types like Extract and Exclude that filter union members.

Template literal types

Template literal types manipulate strings at the type level:

type EventName<T extends string> = `on${Capitalize<T>}`;

type ClickEvent = EventName<"click">; // "onClick"
type FocusEvent = EventName<"focus">; // "onFocus"

With mapped types

type EventHandlers<T extends string> = {
  [K in T as `on${Capitalize<K>}`]: () => void;
};

type MouseEvents = EventHandlers<"click" | "hover" | "focus">;
// { onClick: () => void; onHover: () => void; onFocus: () => void; }

String pattern matching

type CSSProperty = `${string}-${string}`;
type ValidCSS = CSSProperty; // any string with a hyphen

Discriminated unions

Discriminated unions are the most important advanced pattern for modeling state:

type RequestState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: string };

function handleState(state: RequestState<User>) {
  switch (state.status) {
    case "idle":
      return "Ready";
    case "loading":
      return "Loading...";
    case "success":
      return state.data.name; // TypeScript knows data exists
    case "error":
      return state.error;    // TypeScript knows error exists
  }
}

The status property is the discriminant. TypeScript narrows the type automatically in each branch.

Branded types

Branded types prevent mixing types that have the same structure:

type UserId = string & { readonly __brand: unique symbol };
type OrderId = string & { readonly __brand: unique symbol };

function branded<T extends string>(value: T): T & { readonly __brand: unique symbol } {
  return value as any;
}

const userId = branded<UserId>("user-123");
const orderId = branded<OrderId>("order-456");

function getUser(id: UserId) { /* ... */ }
getUser(userId);  // OK
getUser(orderId); // Error: OrderId is not assignable to UserId

Best practices

  • Use utility types instead of manually duplicating type definitions.
  • Prefer discriminated unions over type assertions for state modeling.
  • Build types from small, composable pieces instead of large monolithic definitions.
  • Use conditional types sparingly — if a type is hard to read, simplify the API.
  • Use infer to extract types from libraries instead of manually typing return values.
  • Keep the source of truth in one place and derive everything else.
  • Use branded types when you need nominal typing for IDs or similar values.
  • Treat complex types as a last resort — simple types are easier to maintain.

Common mistakes

  • Over-engineering types when simpler ones would work.
  • Creating deeply nested conditional types that are impossible to debug.
  • Using as assertions instead of proper type narrowing.
  • Not using utility types, leading to duplicated type definitions.
  • Making types too complex for the benefit they provide.
  • Forgetting that types are erased at runtime — no runtime type checking.
  • Not documenting complex types with comments explaining the intent.

What to learn next

You now understand advanced types: utility types, conditional types, mapped types and template literals. From here the natural next step is TypeScript for the fundamentals — annotations, inference, interfaces and generics. Pick a project, refactor some types and let the practice compound.

Utility types vs manual definitions

Utility types derive from existing types, keeping one source of truth. Manual definitions duplicate and drift.

Prefer
interface User {
  id: number;
  name: string;
  email: string;
}

type UserUpdate = Partial<User>;
type UserPreview = Pick<User, "id" | "name">;
Avoid
interface User {
  id: number;
  name: string;
  email: string;
}

interface UserUpdate {
  id?: number;
  name?: string;
  email?: string;
}

Discriminated unions vs type assertions

Discriminated unions give TypeScript enough information to narrow types automatically.

Prefer
type Result =
  | { ok: true; data: User }
  | { ok: false; error: string };

function handle(result: Result) {
  if (result.ok) {
    console.log(result.data);
  }
}
Avoid
interface Result {
  ok: boolean;
  data?: User;
  error?: string;
}

function handle(result: Result) {
  const user = result.data as User;
}

FAQ

Frequently asked questions

Keep learning

Related topics from the roadmap.

$ start learning

Ready to start learning Advanced Types?

Our interactive tutorial walks you through Advanced Types step by step — with quizzes and real code you can run in the browser.