Skip to Content
TypeScript Intermediate: Types That Survive Real Projects
Intermediate
0 likes
Courses
TypeScript Intermediate: Types That Survive Real Projects

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.

Course Home

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 T is a union of the property names of T. For Signup that is "email" | "age" | "newsletter".
  • K in ... means "for each name in that union, call it K".
  • T[K] is the type of that property on T. For K equal to "age", it is number.
  • 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 string as 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
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.