Type expression

Mapped types

  • Combining types
  • Tested with TypeScript 5.9

A mapped type creates an object type by going through a set of keys: { [K in keyof T]?: string } has one optional string property for every property of T. A form's error messages, a set of loading flags or a map of change handlers can follow the shape of the data they belong to, as in the first example.

In JSDoc, mapped types live in a @typedef with @template. Like other object types, they need the double braces of a type literal inside the tag.

At a glance

Use JSDoc in a .js file TypeScript in a .ts file
One property per key /** * @template T * @typedef {{ [K in keyof T]?: string }} FormErrors */ type FormErrors<T> = { [K in keyof T]?: string };
Renaming keys {{ [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] }} { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] }

Examples

form.js form.ts TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. No errors. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. No errors. checked 21 October 2026
/**
 * @template T
 * @typedef {{ [K in keyof T]?: string }} FormErrors
 */

/** @typedef {{ email: string, password: string, age: number }} Signup */

/**
 * @param {Signup} form
 * @returns {FormErrors<Signup>}
 */
export function validate(form) {
  /** @type {FormErrors<Signup>} */
  const errors = {};
  if (!form.email.includes("@")) errors.email = "Enter an email address";
  if (form.password.length < 8) errors.password = "Use at least 8 characters";
  return errors;
}
type FormErrors<T> = { [K in keyof T]?: string };

type Signup = { email: string; password: string; age: number };

export function validate(form: Signup): FormErrors<Signup> {
  const errors: FormErrors<Signup> = {};
  if (!form.email.includes("@")) errors.email = "Enter an email address";
  if (form.password.length < 8) errors.password = "Use at least 8 characters";
  return errors;
}
A mapped type: one property per key of another type.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}
form.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: form.js(8,18): error TS2561: Object literal may only specify known properties, but 'emial' does not exist in type 'FormErrors<{ email: string; password: string; }>'. Did you mean to write 'email'? 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. form.js(8,18): error TS2561: Object literal may only specify known properties, but 'emial' does not exist in type 'FormErrors<{ email: string; password: string; }>'. Did you mean to write 'email'? 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. form.js(8,18): error TS2561: Object literal may only specify known properties, but 'emial' does not exist in type 'FormErrors<{ email: string; password: string; }>'. Did you mean to write 'email'? checked 21 October 2026
/**
 * @template T
 * @typedef {{ [K in keyof T]?: string }} FormErrors
 */

/** @type {FormErrors<{ email: string, password: string }>} */
const errors = { emial: "Enter an email address" };error TS2561: Object literal may only specify known properties, but 'emial' does not exist in type 'FormErrors<{ email: string; password: string; }>'. Did you mean to write 'email'?

export { errors };
A key the source type does not have.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}
getters.js TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. No errors. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. No errors. checked 21 October 2026
/**
 * @template T
 * @typedef {{ [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] }} Getters
 */

/** @type {Getters<{ sku: string, price: number }>} */
const product = {
  getSku: () => "TEE-01",
  getPrice: () => 19.5,
};

product.getPrice().toFixed(2);

export { product };
Renaming keys with as and a template literal.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

Modifiers

? makes every property optional and readonly makes every property read-only; -? and -readonly remove them. Partial<T>, Required<T> and Readonly<T> are mapped types with exactly these modifiers.

Renaming keys with as

An as clause renames each key, often with a template literal type: [K in keyof T as `get${Capitalize<string & K>}`] turns sku into getSku, as the third example shows. Mapping a key to never drops it. TypeScript added key remapping in version 4.1.

Gotchas

  • The mapped type keeps the source's keys exactly, so a misspelled key in an object literal is an excess property (TS2561 with a suggestion, as in the second example).
  • string & K in the renaming example is needed because keyof T can include number and symbol keys, which Capitalize does not accept.

Questions

Can I use mapped types in JSDoc?

Yes, inside a type literal in a @typedef: /** @template T @typedef {{ [K in keyof T]: boolean }} Flags */, with the tags on separate lines if you prefer.

Sources

Last reviewed 20 October 2026.