Skip to content

TypeScript Mapped Types — Complete Guide

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about TypeScript Mapped Types. We cover key concepts, practical examples, and best practices to help you master this topic.

TypeScript mapped types iterate over object keys to transform each property — they are the foundation of utility types like Partial, Required, Readonly, Pick, and Record, and enable custom type transformations that adapt to any object shape.

What You'll Learn

  • Mapped type syntax: [K in keyof T]: T[K]
  • Adding and removing modifiers (?, readonly)
  • Key remapping with as clause (TS 4.1+)
  • Built-in mapped types: Partial, Required, Readonly, Pick, Record
  • Custom mapped types for real-world scenarios

Why It Matters

Without mapped types, every variation of a type (optional version, readonly version, select subset) must be written manually. Mapped types compute these variations automatically, reducing duplication and ensuring consistency when the base type changes.

Real-World Use

Durga Antivirus Pro's configuration system uses a DeepPartial<Config> type for partial config overrides — users can specify only the settings they want to change while keeping full type safety. The Doda Browser extension API uses Pick<Permissions, "tabs" | "storage"> to extract permission subsets.

Learning Path

flowchart LR
  A[Conditional Types] --> B[Mapped Types]
  B --> C[Utility Types]
  B --> D[You Are Here]
  C --> E[Namespaces & Modules]
  E --> F[Declaration Files]

Basic Mapped Type Syntax

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

// This creates a copy of T's shape, but you can transform each property

The [K in keyof T] iterates over every key of T, and T[K] is the value type at that key.

Modifying Properties

Making Properties Optional

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

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

type PartialUser = MyPartial<User>;
// { name?: string; age?: number; email?: string }

Making Properties Readonly

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

type ReadonlyUser = MyReadonly<User>;
// { readonly name: string; readonly age: number; readonly email: string }

Removing Modifiers

Use -? and -readonly to remove modifiers:

type MyRequired<T> = {
  [K in keyof T]-?: T[K];
};

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

interface OptionalUser {
  name?: string;
  age?: number;
}

type RequiredUser = MyRequired<OptionalUser>;
// { name: string; age: number }

The Pick Type

Select a subset of keys:

type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

interface User {
  name: string;
  age: number;
  email: string;
  role: "admin" | "user";
}

type PublicProfile = MyPick<User, "name" | "email">;
// { name: string; email: string }

The Record Type

Create an object type with specified keys and value type:

type MyRecord<K extends keyof any, V> = {
  [P in K]: V;
};

type PageInfo = "home" | "about" | "contact";
type PageData = MyRecord<PageInfo, { title: string; path: string }>;

const pages: PageData = {
  home: { title: "Home", path: "/" },
  about: { title: "About", path: "/about" },
  contact: { title: "Contact", path: "/contact" },
};

Key Remapping with as (TS 4.1+)

You can remap keys using the as clause:

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 }

Filtering Keys

type Methods<T> = {
  [K in keyof T as T[K] extends Function ? K : never]: T[K];
};

type Properties<T> = {
  [K in keyof T as T[K] extends Function ? never : K]: T[K];
};

interface Service {
  name: string;
  start(): void;
  stop(): void;
  version: number;
}

type ServiceMethods = Methods<Service>;
// { start: () => void; stop: () => void }

type ServiceProps = Properties<Service>;
// { name: string; version: number }

Homomorphic vs Non-Homomorphic Mapped Types

Homomorphic mapped types preserve modifiers from the input type (like readonly and ?):

// Homomorphic — preserves modifiers
type Homomorphic<T> = {
  [K in keyof T]: T[K];
};

// Non-homomorphic — does not preserve modifiers
type NonHomomorphic<T> = {
  [K in string]: T extends unknown ? T : never;
};

Pick, Partial, Required, Readonly are homomorphic. Record is non-homomorphic.

Practical Custom Mapped Types

DeepPartial

type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object
    ? T[K] extends Function
      ? T[K]
      : DeepPartial<T[K]>
    : T[K];
};

interface Config {
  server: { host: string; port: number };
  auth: { token: string; ttl: number };
}

const partial: DeepPartial<Config> = {
  server: { host: "localhost" }, // Only override host
};

Serializable

type Serializable<T> = {
  [K in keyof T as T[K] extends Function ? never : K]: T[K];
};

Nullable

type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

Combining Mapped and Conditional Types

type ReadonlyNonFunction<T> = {
  readonly [K in keyof T as T[K] extends Function ? never : K]: T[K];
};

type MutableFunction<T> = {
  -readonly [K in keyof T as T[K] extends Function ? K : never]: T[K];
};

interface ApiService {
  readonly url: string;
  readonly version: number;
  fetch(): Promise<unknown>;
  parse(data: string): void;
}

type ConfigOnly = ReadonlyNonFunction<ApiService>;
// { readonly url: string; readonly version: number }

Common Mistakes

1. Forgetting That Mapped Types Only Work with Object Types

type MyType = Partial<string>;
// Error: Type 'string' does not satisfy the constraint 'object'.

Use extends object constraint to ensure only object types are used.

2. Using in keyof on Non-Object Types

// This works but produces odd results
type ArrayPartial<T> = Partial<T[]>;
// { [x: number]: T | undefined; ... Array methods }

3. Confusing Pick and Record

// Pick selects existing keys from an object type
Pick<User, "name" | "age">

// Record creates a new object type with specified keys
Record<"a" | "b", string>

4. Not Using as Clause for Key Filtering

Before TS 4.1, you couldn't filter keys in mapped types. With as, you can:

// Before (had to use Omit/Exclude)
type WithoutFunctions<T> = Omit<T, keyof {
  [K in keyof T as T[K] extends Function ? K : never]: T[K];
}>;

// After (clean and direct)
type WithoutFunctions<T> = {
  [K in keyof T as T[K] extends Function ? never : K]: T[K];
};

5. Using Readonly<T> When You Mean const

Readonly<T> only makes the top-level properties readonly. For deep immutability, use as const at the value level or a DeepReadonly mapped type.

Practice Questions

  1. What is the syntax for a mapped type that makes all properties optional? { [K in keyof T]?: T[K] } — the ? modifier on the key makes it optional.

  2. How do you remove the readonly modifier from all properties? { -readonly [K in keyof T]: T[K] } — the -readonly prefix removes the modifier.

  3. What is the difference between Pick<T, K> and Record<K, V>? Pick selects existing keys from an existing type. Record creates a new object type with specified keys and value type.

  4. How does the as clause in mapped types work? It remaps keys by specifying a new key expression, supporting filtering and transformation of key names.

Challenge: Create a DeepNonNullable<T> mapped type that recursively removes null and undefined from all properties, including nested objects.

FAQ

What is a homomorphic mapped type?

A mapped type that preserves modifiers (readonly, optional) from the input type. It uses keyof T explicitly. Pick, Partial, Required, Readonly are homomorphic.

Can mapped types work with tuples?

Yes. Mapped types applied to tuples preserve the tuple length and indexed positions.

What is the `-?` syntax in mapped types?

It removes the optional modifier from properties, making them required. +? can also be used (explicitly adding optional), but ? alone is shorthand for +?.

How do I create a mapped type that renames keys?

Use the as clause: [K in keyof T as \new${K}`]: T[K]`

Can mapped types be combined with template literals?

Yes, this is a powerful pattern for generating getter/setter types from property names.

Mini Project: Form Validation Types

// src/validation.ts

type ValidationRule<T> = {
  required?: boolean;
  minLength?: number;
  maxLength?: number;
  pattern?: RegExp;
  custom?: (value: T) => boolean;
};

type ValidationSchema<T> = {
  [K in keyof T]-?: ValidationRule<T[K]>;
};

type ValidationErrors<T> = {
  [K in keyof T]?: string[];
};

function validate<T extends Record<string, unknown>>(
  data: T,
  schema: ValidationSchema<T>
): ValidationErrors<T> {
  const errors: ValidationErrors<T> = {};

  for (const key in schema) {
    const rule = schema[key];
    const value = data[key];
    const fieldErrors: string[] = [];

    if (rule.required && (value === undefined || value === null || value === "")) {
      fieldErrors.push(`${key} is required`);
    }
    if (rule.minLength !== undefined && typeof value === "string" && value.length < rule.minLength) {
      fieldErrors.push(`${key} must be at least ${rule.minLength} characters`);
    }
    if (rule.pattern && typeof value === "string" && !rule.pattern.test(value)) {
      fieldErrors.push(`${key} does not match required pattern`);
    }

    if (fieldErrors.length > 0) {
      errors[key] = fieldErrors;
    }
  }

  return errors;
}

interface LoginForm {
  email: string;
  password: string;
}

const schema: ValidationSchema<LoginForm> = {
  email: { required: true, pattern: /^[^@]+@[^@]+\.[^@]+$/ },
  password: { required: true, minLength: 8, maxLength: 128 },
};

const data: LoginForm = { email: "invalid", password: "123" };
const errors = validate(data, schema);

if (Object.keys(errors).length > 0) {
  for (const [field, fieldErrors] of Object.entries(errors)) {
    console.log(`${field}: ${fieldErrors?.join(", ")}`);
  }
}
// Output:
// email: email does not match required pattern
// password: password must be at least 8 characters

What's Next

Now explore TypeScript's built-in utility types:

Lesson Description
{{< ref "/programming-languages/typescript/13-conditional-types" >}} Review conditional types
{{< ref "/programming-languages/typescript/15-utility-types" >}} Omit, Extract, Exclude, NonNullable, ReturnType
{{< ref "/programming-languages/typescript/16-namespaces-modules" >}} Namespaces and modules

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro