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
/**
* @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;
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} /**
* @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 };jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} /**
* @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 };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 & Kin the renaming example is needed becausekeyof Tcan include number and symbol keys, whichCapitalizedoes 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
- Mapped Types · TypeScript handbook
- TypeScript 4.1: key remapping in mapped types · TypeScript release notes
Last reviewed 20 October 2026.