Skip to content
JL

Home

About

Blog

Contact

Shop

Portfolio

Privacy

TOS

Click to navigate

  1. Home
  2. Joshua R. Lehman's Blog
  3. Mapped Types: Transforming Types in TypeScript

Table of contents

  • Share on X
  • Discuss on X

Related Articles

Indexed Access Types in TypeScript
TypeScript
7m
Jul 26, 2026

Indexed Access Types in TypeScript

Indexed access types use the T[K] syntax to look up property types — the same bracket notation you use at runtime, but at the type level. They let you extract any type that lives inside another type without duplicating it.

#Indexed Access Types#TypeScript Type Lookup+5
keyof and typeof Operators in TypeScript
TypeScript
8m
Jul 19, 2026

keyof and typeof Operators in TypeScript

keyof extracts the union of property names from a type. typeof extracts the type of a value at compile time. Together they bridge the gap between your runtime values and your type-level code — letting you derive types from what already exists.

#keyof Operator#typeof Operator+5
The infer Keyword in TypeScript
TypeScript
9m
Jul 5, 2026

The infer Keyword in TypeScript

The infer keyword is TypeScript's type-level pattern match. Inside a conditional type, it captures an unknown part of a type so you can use it on the right-hand side — the same way a regex capture group pulls out part of a string.

#infer Keyword#TypeScript Conditional Types+5
Ask me anything! 💬

© Joshua R. Lehman

Full Stack Developer

Crafted with passion • Built with modern web technologies

2026 • All rights reserved

Contents

  • What Are Mapped Types
  • Basic Mapped Type Syntax
  • Mapping with Modifiers
    • Adding and Removing Modifiers
  • Key Remapping with as
  • Filtering Properties
  • Real-World Patterns
  • Best Practices
  • What Is Next
  • Key Takeaways
TypeScript

Mapped Types: Transforming Types in TypeScript

July 12, 2026•7 min read
Joshua R. Lehman
Joshua R. Lehman
Author
TypeScript mapped types transforming object type properties systematically
Mapped Types: Transforming Types in TypeScript

Mapped types are TypeScript's equivalent of Array.map for type transformations. Just as map takes an array and produces a new array by applying a function to every element, a mapped type takes an object type and produces a new type by applying a transformation to every property. They're the mechanism behind Partial<T>, Required<T>, Readonly<T>, and Record<K, V> — and once you understand the pattern, you can build your own. In this post, you'll learn how to write mapped types, add and remove property modifiers, remap keys, and filter properties using conditional types.

Mapped Type Mental Model

A mapped type says: "For every key K in this union of keys, produce a property with key K and value type ...." The [K in Keys] syntax is the iteration — K is a type variable that takes each value in Keys one at a time.

What Are Mapped Types

A mapped type creates a new object type by iterating over a set of keys. The most basic form uses keyof to iterate over the keys of an existing type:

type Copy<T> = {
  [K in keyof T]: T[K];
};
 
interface User {
  id: number;
  name: string;
  active: boolean;
}
 
type CopiedUser = Copy<User>;
// { id: number; name: string; active: boolean }
// Structurally identical to User

The [K in keyof T] part iterates: K takes the value "id", then "name", then "active". For each, T[K] is the type of that property — number, string, boolean.

Basic Mapped Type Syntax

The real power comes from transforming the value type on the right-hand side:

// Make every property a string
type Stringify<T> = {
  [K in keyof T]: string;
};
 
// Wrap every property in an array
type Arrayify<T> = {
  [K in keyof T]: T[K][];
};
 
// Wrap every property in Promise
type Promisify<T> = {
  [K in keyof T]: Promise<T[K]>;
};
 
type User = { id: number; name: string };
 
type StringUser = Stringify<User>; // { id: string; name: string }
type ArrayUser = Arrayify<User>; // { id: number[]; name: string[] }
type AsyncUser = Promisify<User>; // { id: Promise<number>; name: Promise<string> }

You can also map over a specific union of string literal types, not just keyof:

type Flags = {
  [K in "read" | "write" | "delete"]: boolean;
};
// { read: boolean; write: boolean; delete: boolean }

Mapping with Modifiers

Mapped types can add or remove the ? (optional) and readonly modifiers. This is how Partial<T> and Required<T> work:

// TypeScript's built-in Partial<T>
type Partial<T> = {
  [K in keyof T]?: T[K];
};
 
// TypeScript's built-in Required<T>
type Required<T> = {
  [K in keyof T]-?: T[K]; // The minus removes the optional modifier
};
 
// TypeScript's built-in Readonly<T>
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};
 
// Remove readonly from all properties
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

Adding and Removing Modifiers

The + prefix adds a modifier (it's the default), and the - prefix removes it:

interface Config {
  readonly host: string;
  readonly port?: number;
  readonly debug?: boolean;
}
 
// Remove readonly and make everything required
type WritableRequired<T> = {
  -readonly [K in keyof T]-?: T[K];
};
 
type MutableConfig = WritableRequired<Config>;
// { host: string; port: number; debug: boolean }

Mutable Is Not Built In

TypeScript doesn't ship a Mutable<T> utility type out of the box. The -readonly [K in keyof T]: T[K] pattern is the standard way to write it yourself — and it comes up often in testing code where you need to set read-only fields.

Key Remapping with as

TypeScript 4.1 added key remapping via the as clause. This lets you transform the key names themselves — not just their value types:

// Prefix all keys with "get"
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
 
interface User {
  id: number;
  name: string;
}
 
type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string }

The as clause takes a template literal type to transform the key. Capitalize<string & K> capitalizes the first letter of each key name.

You can also remap to completely different keys:

type EventMap<T> = {
  [K in keyof T as `on${Capitalize<string & K>}`]: (value: T[K]) => void;
};
 
type FormEvents = EventMap<{ submit: FormData; reset: void; change: string }>;
// { onSubmit: (value: FormData) => void; onReset: (value: void) => void; onChange: (value: string) => void }

Filtering Properties

Combine key remapping with conditional types to filter out properties:

// Keep only properties whose value type extends a given type
type PickByValue<T, V> = {
  [K in keyof T as T[K] extends V ? K : never]: T[K];
};
 
interface Mixed {
  id: number;
  name: string;
  active: boolean;
  count: number;
  label: string;
}
 
type StringProperties = PickByValue<Mixed, string>;
// { name: string; label: string }
 
type NumberProperties = PickByValue<Mixed, number>;
// { id: number; count: number }

When the as expression evaluates to never, that key is excluded from the result — just like never drops out of union types.

never Filters Keys

In a mapped type's as clause, returning never removes that key from the output. This is a powerful filtering mechanism, but it only works in key remapping — not in the value position.

Real-World Patterns

Here's a real-world pattern for building a form validation schema from a data type:

type ValidationRule<T> = {
  required?: boolean;
  validate?: (value: T) => string | undefined;
};
 
type FormSchema<T> = {
  [K in keyof T]: ValidationRule<T[K]>;
};
 
interface LoginForm {
  email: string;
  password: string;
  rememberMe: boolean;
}
 
const loginSchema: FormSchema<LoginForm> = {
  email: {
    required: true,
    validate: (v) => (v.includes("@") ? undefined : "Invalid email"),
  },
  password: {
    required: true,
    validate: (v) => (v.length >= 8 ? undefined : "Too short"),
  },
  rememberMe: {
    required: false,
  },
};

TypeScript enforces that loginSchema has exactly the same keys as LoginForm, each with the right validation rule shape. Add or remove a field from LoginForm and the schema will break at compile time until you update it.

Keeping Types in Sync

Mapped types are what keep your derived types synchronized with their source. When you add a field to LoginForm, TypeScript immediately flags every mapped type that needs updating — a compile-time refactoring guide.

Best Practices

Use mapped types for structural transformations, not business logic. If you're transforming types based on application rules, consider whether a simpler approach (discriminated union, interface extension) would be clearer.

Compose mapped types. You can chain them: Readonly<Partial<T>> is perfectly valid and readable.

Name intermediate types. If a mapped type is complex, break it into named steps:

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

Avoid mutating modifiers unnecessarily. Removing readonly everywhere is a smell. If you need to mutate a readonly value, consider whether your design should change rather than working around the constraint.

Anti-Pattern

Don't use mapped types when Pick, Omit, or Partial already does what you need. Reaching for a custom mapped type before checking the standard library is premature complexity.

What Is Next

You've seen mapped types iterate over keyof T — but what exactly does keyof produce? The next post covers the keyof and typeof operators in depth: how they work, what types they produce, and how they combine to build powerful type queries.

Key Takeaways

  • Mapped types use [K in keyof T]: ... to iterate over the keys of a type and produce a new type
  • The ? and readonly modifiers can be added or removed using +?, -?, +readonly, and -readonly
  • The as clause in key remapping transforms key names and can filter keys by returning never
  • Combining mapped types with conditional types enables property filtering by value type
  • Standard library types Partial, Required, Readonly, and Record are all mapped types

Mapped types are the backbone of TypeScript's structural transformation system. With them, you can derive any variation of a type — optional, readonly, promisified, validated — without ever writing the same type shape twice.