A function whose JSDoc says @returns {item is Product} returns a boolean, and TypeScript also learns from it: wherever the call returns true, item is a Product. Such a function is a user-defined type guard. It narrows inside if (isProduct(item)) and turns items.filter(isProduct) into a Product[].
With a plain @returns {boolean}, the same function narrows nothing, as the second example shows: TypeScript does not look inside the function body to work out what true means.
At a glance
| Use | JSDoc in a .js file | TypeScript in a .ts file |
|---|---|---|
| A type guard | /**
* @param {Product | Bundle} item
* @returns {item is Product}
*/ | function isProduct(item: Product | Bundle): item is Product |
Examples
/** @typedef {{ kind: "product", sku: string, price: number }} Product */
/** @typedef {{ kind: "bundle", skus: string[] }} Bundle */
/**
* @param {Product | Bundle} item
* @returns {item is Product}
*/
export function isProduct(item) {
return item.kind === "product";
}
/** @type {(Product | Bundle)[]} */
const items = [
{ kind: "product", sku: "TEE-01", price: 19.5 },
{ kind: "bundle", skus: ["TEE-01", "MUG-07"] },
];
const products = items.filter(isProduct);
products.map((p) => p.price);
const first = items[0];
if (isProduct(first)) first.sku.toLowerCase();type Product = { kind: "product"; sku: string; price: number };
type Bundle = { kind: "bundle"; skus: string[] };
export function isProduct(item: Product | Bundle): item is Product {
return item.kind === "product";
}
const items: (Product | Bundle)[] = [
{ kind: "product", sku: "TEE-01", price: 19.5 },
{ kind: "bundle", skus: ["TEE-01", "MUG-07"] },
];
const products = items.filter(isProduct);
products.map((p) => p.price);
const first = items[0];
if (isProduct(first)) first.sku.toLowerCase();jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} /** @typedef {{ kind: "product", sku: string }} Product */
/** @typedef {{ kind: "bundle", skus: string[] }} Bundle */
/**
* @param {Product | Bundle} item
* @returns {boolean}
*/
function isProduct(item) {
return item.kind === "product";
}
/** @param {Product | Bundle} item */
export function skuOf(item) {
if (isProduct(item)) return item.sku;error TS2339: Property 'sku' does not exist on type 'Product | Bundle'.
Property 'sku' does not exist on type 'Bundle'.
return null;
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} /** @type {(string | undefined)[]} */
const skus = ["TEE-01", undefined, "MUG-07"];
// The arrow function is inferred as (sku) => sku is string, so the result is string[].
const present = skus.filter((sku) => sku !== undefined);
present.map((sku) => sku.toLowerCase());
export { present };jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} Inferred predicates
TypeScript 5.5 added inferred type predicates: a function that returns a simple check on its parameter, such as (sku) => sku !== undefined, is treated as a predicate without any annotation. That is why the third example's filter returns string[]. Functions with more complex bodies, or that are declared @returns {boolean}, still need the explicit predicate.
Keeping them honest
TypeScript trusts the predicate: if isProduct returns true for a bundle, the narrowing is wrong and nothing reports it. Keep guards small and make them test the property that actually tells the types apart, like the kind field here.
Gotchas
- The name in the predicate must be one of the function's parameters:
@returns {item is Product}for a parameter nameditem. - For checks that should throw rather than return false, use an assertion function instead.
Questions
How do I write a type guard in JSDoc?
Return a boolean and declare it as a predicate: @returns {value is string} on a function whose parameter is named value.
Sources
- Narrowing: using type predicates · TypeScript handbook
- TypeScript 5.5: inferred type predicates · TypeScript release notes
Last reviewed 20 October 2026.