A constraint says what a type parameter must be assignable to. In JSDoc it goes in braces between @template and the name: @template {{ updatedAt: Date }} T is TypeScript's T extends { updatedAt: Date }. Inside the function you may then use updatedAt, and callers must pass something that has it.
Constraints can refer to earlier type parameters. @template T followed by @template {keyof T} K gives the classic property getter, where the key must be one of the object's own keys (TS2345 for "colr", as the first example shows).
At a glance
| Use | JSDoc in a .js file | TypeScript in a .ts file |
|---|---|---|
| A constraint | /** @template {{ updatedAt: Date }} T */ | <T extends { updatedAt: Date }> |
| Depending on another parameter | /**
* @template T
* @template {keyof T} K
*/ | <T, K extends keyof T> |
Examples
/**
* @template T
* @template {keyof T} K
* @param {T} object
* @param {K} key
* @returns {T[K]}
*/
export function prop(object, key) {
return object[key];
}
const tee = { sku: "TEE-01", price: 19.5 };
prop(tee, "price").toFixed(2);
prop(tee, "colr");error TS2345: Argument of type '"colr"' is not assignable to parameter of type '"price" | "sku"'.export function prop<T, K extends keyof T>(object: T, key: K): T[K] {
return object[key];
}
const tee = { sku: "TEE-01", price: 19.5 };
prop(tee, "price").toFixed(2);
prop(tee, "colr");error TS2345: Argument of type '"colr"' is not assignable to parameter of type '"price" | "sku"'.jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} /**
* @template {{ updatedAt: Date }} T
* @param {T[]} records
* @returns {T | undefined}
*/
export function latest(records) {
return [...records].sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime())[0];
}
latest([{ id: "A-100", updatedAt: new Date() }])?.id;
latest([{ id: "A-101", updatedAt: "2026-10-20" }]);error TS2322: Type 'string' is not assignable to type 'Date'.jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} /**
* @template {string} K, V
* @param {K} key
* @param {V} value
*/
export function entry(key, value) {
return { [key]: value };
}
entry("sku", 42);
entry(42, "sku");error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} One name per constraint
A constraint applies only to the first name after it. In @template {string} K, V, K must be a string and V is unconstrained, which is easy to misread. The third example shows the first argument checked against string and the second accepting a number. Put each constrained parameter on its own @template line.
Constraints keep the specific type
A constrained parameter still infers the caller's full type. latest(records) in the second example returns the records' own type, id included, not just { updatedAt: Date }. That is the difference from a plain parameter type.
Gotchas
- When an argument does not meet the constraint, the error may be reported on a property of the argument rather than on the call, as with
updatedAt: "2026-10-20"(TS2322). - A constraint and a default can be combined:
@template {string | number} [T=number].
Questions
How do I write T extends something in JSDoc?
Put the constraint in braces before the name: @template {Something} T.
Sources
- Generics: generic constraints · TypeScript handbook
- JSDoc Reference: @template · TypeScript handbook
Last reviewed 20 October 2026.