Mapped Types You Can Actually Read
Read a mapped type in four pieces, change its modifiers, tie validators to a type and rename keys with as. After this, you can read the built-in utility types and write one when you need it.
Course content
Sign in to save your progress.
-
Generics Without the Fog
-
Generic Functions That Keep the Input Type
-
Generic Types for Lists, Results and Caches
-
-
-
Types That Transform Other Types
-
Utility Types: Partial, Pick, Omit and Record
-
Mapped Types You Can Actually Read
-
-
-
Working With Code You Didn't Write
-
@types Packages and When DefinitelyTyped Is Wrong
-
Typing fetch JSON Without Lying to Yourself
-
-
-
Classes and Modules in TypeScript
-
Classes With public, private and protected
-
Modules, Exports and Avoiding Circular Types
-
-
-
Strictness That Pays Off
-
unknown vs any and Safe Parsing
-
Discriminated Unions for API Responses That Vary
-
-
- Final Check: TypeScript Intermediate
Skip ahead: If { [K in keyof T]: T[K] } already reads as "for each key of T" to you, skip to "A mapped type that does real work", then continue.
A mapped type looks like line noise the first time: brackets, in, keyof, a question mark in the wrong place. It is one idea, though. It takes the keys of a type and builds a new type by doing the same thing to each key. Partial, Readonly, Pick and Record from the last lesson are all written this way. Once you can read one, you can read the rest, and you can write your own when the built-in ones are not enough.
Read it in four pieces
type Signup = { email: string; age: number; newsletter: boolean };
type MyPartial<T> = { [K in keyof T]?: T[K] };
const draft: MyPartial<Signup> = { email: "ana@example.com" };
const wrong: MyPartial<Signup> = { emial: "ana@example.com" };
mypartial.ts(6,36): error TS2561: Object literal may only specify known properties, but 'emial' does not exist in type 'MyPartial<Signup>'. Did you mean to write 'email'?
Take { [K in keyof T]?: T[K] } one piece at a time.
keyof Tis a union of the property names ofT. ForSignupthat is"email" | "age" | "newsletter".K in ...means "for each name in that union, call itK".T[K]is the type of that property onT. ForKequal to"age", it isnumber.- The
?before the colon adds the optional marker to every property in the result.
So the whole line says: for each key of T, make a property with the same name and the same type, but optional. That is exactly what Partial does. The draft line compiles and the misspelled one does not.
Change the modifiers
A minus sign removes a modifier. A plus sign, which is the default, adds one.
type Signup = { readonly email: string; age?: number };
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type Complete<T> = { [K in keyof T]-?: T[K] };
const a: Mutable<Signup> = { email: "ana@example.com" };
a.email = "ben@example.com";
const b: Complete<Signup> = { email: "ana@example.com" };
modifiers.ts(9,7): error TS2741: Property 'age' is missing in type '{ email: string; }' but required in type 'Complete<Signup>'.
-readonly makes the properties writable, which is why the assignment to a.email is fine. -? makes the optional age required, which is why b is missing it. Required<T> is the second one with a name.
A mapped type that does real work
Say you validate a sign-up form. Each field needs its own check, and each check takes the type of that field. A mapped type ties the two together.
type Signup = { email: string; age: number; newsletter: boolean };
type Validators<T> = { [K in keyof T]: (value: T[K]) => boolean };
const validators: Validators<Signup> = {
email: (v) => v.includes("@"),
age: (v) => v >= 18,
newsletter: () => true,
};
function validate<T>(data: T, rules: Validators<T>): (keyof T)[] {
const failed: (keyof T)[] = [];
for (const key in rules) {
if (!rules[key](data[key])) {
failed.push(key);
}
}
return failed;
}
const form = { email: "ana.example.com", age: 20, newsletter: false };
console.log(validate(form, validators));
[ 'email' ]
The email has no @, so email fails. Note that you did not annotate v in any of the three functions. The compiler knows v is a string in email and a number in age, because T[K] follows the key. Now try writing a check that does not fit the field.
type Signup = { email: string; age: number; newsletter: boolean };
type Validators<T> = { [K in keyof T]: (value: T[K]) => boolean };
const validators: Validators<Signup> = {
email: (v) => v.includes("@"),
age: (v) => v.length > 2,
newsletter: () => true,
};
validators_bad.ts(7,17): error TS2339: Property 'length' does not exist on type 'number'.
A number has no length. And if you add a field to Signup and forget its check, the compiler tells you, because the mapped type wants one function per key.
Rename keys with as
Since TypeScript 4.1 you can change the name of each key while you map it, and drop keys by mapping them to never.
type Signup = { email: string; age: number; newsletter: boolean };
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
const getters: Getters<Signup> = {
getEmail: () => "ana@example.com",
getAge: () => 20,
getNews: () => true,
};
const strings: OnlyStrings<Signup> = { email: "ana@example.com", age: 20 };
remap.ts(14,3): error TS2353: Object literal may only specify known properties, and 'getNews' does not exist in type 'Getters<Signup>'.
remap.ts(17,66): error TS2353: Object literal may only specify known properties, and 'age' does not exist in type 'OnlyStrings<Signup>'.
Getters turns email into getEmail and keeps the return type. OnlyStrings keeps a key only when its value is a string, so age is not allowed. These are the two tricks you will see most often. Do not go further than that until you need to.
⚠ Warning: A clever mapped type that nobody on your team can read is a cost. If a plain interface of six lines does the job, write the interface.
Common mistakes
- Using
stringas the key type when you have a fixed list of names.Record<string, T>accepts any typo. - Mapping over a type that has an index signature and wondering why the result is so loose.
- Forgetting that the modifier goes before the closing bracket:
-?and-readonly, not?-. - Building a mapped type to avoid writing three lines of code once.
💡 Tip: Write the type you want by hand for one concrete case first, then turn the repeated part into K in keyof T.
If you skip this, you will read Partial<T> as a spell and not as a rule, and you will not know where to look when a utility type does something strange. Which type in your project has a second copy that a mapped type could replace?
About
Read a mapped type in four pieces, change its modifiers, tie validators to a type and rename keys with as. After this, you can read the built-in utility types and write one when you need it.
- Duration
- 10 min
- Last update
- 10/20/2026
Comments (0)
Sign in to commentNo comments yet.