TypeScript Mapped Types — Complete Guide
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
asclause (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
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.How do you remove the
readonlymodifier from all properties?{ -readonly [K in keyof T]: T[K] }— the-readonlyprefix removes the modifier.What is the difference between
Pick<T, K>andRecord<K, V>?Pickselects existing keys from an existing type.Recordcreates a new object type with specified keys and value type.How does the
asclause 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
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