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

Utility Types Part 1: Partial and Required
TypeScript
9m
Aug 16, 2026

Utility Types Part 1: Partial and Required

TypeScript ships with a set of built-in generic types — called utility types — that perform common type transformations on existing types. Rather than manually rewriting every property of an object type, you pass your type to a utility type and receive a transformed version. Partial and Required are the two complementary utility types that control whether every property in an object is optional or required.

#Utility Types#Partial+5
Recursive Types in TypeScript
TypeScript
10m
Aug 9, 2026

Recursive Types in TypeScript

Some data structures are inherently self-referential — a tree node that contains other tree nodes, a JSON value that can itself be a JSON object containing more JSON values, a linked list whose nodes point to other nodes of the same type. TypeScript's recursive types handle all of these by allowing a type alias to reference itself in its own definition.

#Recursive Types#TypeScript Advanced Types+5
Template Literal Types in TypeScript
TypeScript
9m
Aug 2, 2026

Template Literal Types in TypeScript

Template literal types bring JavaScript's template literal syntax to the type level. They let you construct new string literal types by combining existing ones — the same way a template literal assembles a string at runtime, but resolved entirely at compile time.

#Template Literal Types#String Manipulation 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.