TypeScript Mapped Types: Readonly, Partial from Scratch
Build Partial, Readonly, Pick, and Record yourself with mapped types, modifier control, and key remapping — the toolkit behind every utility type.
What you'll learn
- ✓The { [K in keyof T]: ... } shape
- ✓The + and - modifiers for readonly and optional
- ✓Key remapping with as
- ✓How to build Partial, Readonly, Pick, Record from scratch
- ✓A preview of template literal types
Prerequisites
- •Comfortable with generics and conditional types — see Conditional Types
If conditional types are control flow at the type level, mapped types are loops. They walk the keys of one type and produce a new type, one property at a time. Almost every utility type in lib.es5.d.ts is a one-liner mapped type — and once you can write them, you can also debug them, which is where most of the payoff lives.
The basic shape
A mapped type iterates over a union of keys and produces an object type:
type Stringify<T> = {
[K in keyof T]: string;
};
interface User { id: number; name: string; age: number; }
type UserAsStrings = Stringify<User>;
// { id: string; name: string; age: string }
Read it as: “for each K in the union keyof T, give the new type a property K whose value type is string.” It is a for loop, but the body produces a property instead of running a side effect.
You can use T[K] to look up the original property’s type:
type Identity<T> = {
[K in keyof T]: T[K];
};
type Same = Identity<User>; // User, structurally
That is the no-op mapped type. Useful as a starting point — every utility we write below is a modification of it.
Building Partial
Partial<T> makes every property optional. The ? modifier handles it:
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
type PartialUser = MyPartial<User>;
// { id?: number; name?: string; age?: number }
You can apply it for real-world code like a settings updater:
function updateUser(id: string, patch: MyPartial<User>): void {
// any subset of User keys is fine here
}
updateUser('u1', { name: 'Alice' }); // ok
updateUser('u1', { age: 30, name: 'Bob' }); // ok
Building Readonly
Readonly<T> marks every property readonly:
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
const u: MyReadonly<User> = { id: 1, name: 'A', age: 30 };
u.name = 'B'; // Error: Cannot assign to 'name' because it is a read-only property
Adding and removing modifiers
TypeScript lets you add modifiers with + (the default) or remove them with -. The remove form is what powers Required and Mutable:
// Strip optionality from every property
type MyRequired<T> = {
[K in keyof T]-?: T[K];
};
// Strip readonly from every property
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
interface Config {
readonly host: string;
port?: number;
}
type StrictConfig = MyRequired<Mutable<Config>>;
// { host: string; port: number }
+? and +readonly exist for symmetry but are rarely written — they are the default.
Try it yourself. Write DeepReadonly<T> that applies readonly recursively to every nested object property. Use a conditional to detect objects: T[K] extends object ? DeepReadonly<T[K]> : T[K]. Watch out for arrays and functions, which are also object.
Building Pick
Pick<T, K> keeps only the listed keys. The trick: constrain K to be a subset of keyof T, then iterate over K instead of keyof T:
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
type UserNameAndId = MyPick<User, 'id' | 'name'>;
// { id: number; name: string }
P runs over the union you passed in. Because K extends keyof T, the lookup T[P] is always valid.
Building Record
Record<K, V> builds an object type with the given keys and a single value type. The keys can be any subtype of string | number | symbol:
type MyRecord<K extends keyof any, V> = {
[P in K]: V;
};
type Roles = MyRecord<'admin' | 'editor' | 'viewer', boolean>;
// { admin: boolean; editor: boolean; viewer: boolean }
type CountById = MyRecord<string, number>;
// { [x: string]: number }
When K is a string literal union, you get an exact object type. When K is string, you get an index signature. Same machinery, different shapes.
Key remapping with as
Since TypeScript 4.1, you can transform the key during the mapping with as:
// Prefix every key with 'get' and capitalise it
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 ${...} syntax is a template literal type — a string at the type level, with the same interpolation rules as runtime template literals. Capitalize<S> is a built-in intrinsic.
You can also drop keys by mapping them to never. Anything keyed by never disappears:
// Remove all keys of type V
type Without<T, V> = {
[K in keyof T as T[K] extends V ? never : K]: T[K];
};
interface Mixed { id: number; name: string; flag: boolean; size: number; }
type NoBools = Without<Mixed, boolean>;
// { id: number; name: string; size: number }
That is Omit-by-value-type in three lines.
Building Omit
The built-in Omit<T, K> is itself a mapped type using key remapping:
type MyOmit<T, K extends keyof any> = {
[P in keyof T as P extends K ? never : P]: T[P];
};
type UserWithoutAge = MyOmit<User, 'age'>;
// { id: number; name: string }
The internal definition in lib.es5.d.ts uses a slightly different formulation with Pick and Exclude, but the behaviour is identical.
Try it yourself. Write RenameKey<T, From extends keyof T, To extends string> that renames a single property. The mapping says: “if the key is From, rewrite it to To; otherwise keep it.” Test on User by renaming 'id' to 'userId'.
Template literal types — a preview
Combined with mapped types, template literal types unlock string-shape APIs that used to need runtime checks. A taste:
type EventMap = {
click: { x: number; y: number };
hover: { target: string };
};
type Handlers<E> = {
[K in keyof E as `on${Capitalize<string & K>}`]:
(event: E[K]) => void;
};
type UIHandlers = Handlers<EventMap>;
// {
// onClick: (event: { x: number; y: number }) => void;
// onHover: (event: { target: string }) => void;
// }
Libraries like React’s typed props, tRPC’s procedure routers, and SQL builders lean heavily on this combination. We mention it here because mapped types are the gateway — as is where the door opens. The conditional types in TypeScript Conditional Types are the other half of the toolkit.
A worked example: form schemas
A schema describes the shape of a form. Derived types describe its values, errors, and touched fields:
type FieldType = 'string' | 'number' | 'boolean';
type Schema = Record<string, FieldType>;
// Type of a field's value
type ValueOf<F extends FieldType> =
F extends 'string' ? string :
F extends 'number' ? number :
boolean;
// All values inferred from the schema
type Values<S extends Schema> = {
[K in keyof S]: ValueOf<S[K]>;
};
// All errors as optional strings
type Errors<S extends Schema> = {
[K in keyof S]?: string;
};
// All touched flags
type Touched<S extends Schema> = {
[K in keyof S]: boolean;
};
const signupSchema = {
email: 'string',
age: 'number',
newsletter: 'boolean',
} as const satisfies Schema;
type SignupValues = Values<typeof signupSchema>;
// { email: string; age: number; newsletter: boolean }
type SignupErrors = Errors<typeof signupSchema>;
// { email?: string; age?: string; newsletter?: string }
One schema, three derived types, zero duplication. Add a field to the schema and every dependent type updates automatically.
Pitfalls
A short list of foot-guns:
keyof Tonanyisstring | number | symbol. ConstrainT extends objectto keep things sensible.- Index signatures vs literal keys. Mapping over
keyof { [k: string]: V }gives youstring, not a specific key set. - Distribution sneaks in. When the mapped type’s body uses a conditional with a naked parameter, you may see surprising unions — wrap with
[T[K]] extends [...]to lock in. as neverremoves keys, but only inside the key position. Setting a value toneverkeeps the key with an unusable type.
Recap
You now know:
{ [K in keyof T]: ... }walks the keys ofTand builds a new object type?andreadonlywork as modifiers;+/-add or remove themasremaps keys, including template-literal renames andnever-removalPartial,Readonly,Required,Pick,Omit, andRecordare all small mapped types- Combined with conditional types, mapped types build entire schemas from a single source
Next steps
Conditional types and mapped types are the two engines behind almost every advanced TypeScript pattern. The natural next steps are template literal types in depth, variance and assignability, and the satisfies operator — each a small post that pays off across every project you touch.
Until then, open lib.es5.d.ts in your editor and read the definitions of Partial, Required, Pick, and Omit. You can now read all four at a glance — which means you can write the next one yourself.
Questions or feedback? Email codeloomdevv@gmail.com.