The short answer
Return a boolean, and say what true means in the @returns tag: @returns {value is Product}:
/**
* @typedef {object} Product
* @property {string} sku
* @property {number} price
*/
/**
* @param {unknown} value
* @returns {value is Product}
*/
export function isProduct(value) {
return (
typeof value === "object" &&
value !== null &&
"sku" in value &&
typeof value.sku === "string" &&
"price" in value &&
typeof value.price === "number"
);
}
/** @param {unknown[]} rows */
export function totalPrice(rows) {
return rows.filter(isProduct).reduce((sum, product) => sum + product.price, 0);
}
/** @param {unknown} row */
export function label(row) {
return isProduct(row) ? `${row.sku}: ${row.price}` : "unknown row";
}interface Product {
sku: string;
price: number;
}
export function isProduct(value: unknown): value is Product {
return (
typeof value === "object" &&
value !== null &&
"sku" in value &&
typeof value.sku === "string" &&
"price" in value &&
typeof value.price === "number"
);
}
export function totalPrice(rows: unknown[]) {
return rows.filter(isProduct).reduce((sum, product) => sum + product.price, 0);
}
export function label(row: unknown) {
return isProduct(row) ? `${row.sku}: ${row.price}` : "unknown row";
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} After isProduct(row) returns true, row is a Product in that branch, and rows.filter(isProduct) returns Product[], so product.price needs no further checks.
Why it works
value is Product is a type predicate. It tells tsc: when this function returns true, the argument passed as value is a Product. Callers narrow on it the same way they narrow on typeof or instanceof, and Array.prototype.filter has an overload that uses a predicate to type its result.
Since TypeScript 5.5, tsc infers a predicate for a small function like (count) => count !== undefined on its own, so this filter returns number[] with no JSDoc at all:
/** @type {(number | undefined)[]} */
const counts = [4, undefined, 12];
const known = counts.filter((count) => count !== undefined);
console.log(known.map((count) => count.toFixed(0)));jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} The inference covers checks that narrow the parameter itself, such as !== undefined or typeof. For isProduct, whose checks are on its properties, tsc infers no predicate at all, so the explicit @returns {value is Product} stays necessary.
The same thing in TypeScript
The TypeScript equivalent tab puts the predicate in the return position: function isProduct(value: unknown): value is Product. The JSDoc form is the same text inside @returns braces.
What goes wrong
With @returns {boolean}, the same checks still run, but the caller learns nothing from them, and row stays unknown (TS18046):
/**
* @param {unknown} value
* @returns {boolean}
*/
export function isProduct(value) {
return typeof value === "object" && value !== null && "sku" in value && "price" in value;
}
/** @param {unknown} row */
export function label(row) {
return isProduct(row) ? `${row.sku}` : "unknown row";error TS18046: 'row' is of type 'unknown'.
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} A misspelled parameter name in the predicate is worse, because nothing says so. In a .ts file item is Product on a function whose parameter is value is TS1225 (“Cannot find parameter”); in a .js file TypeScript 5.9 reports nothing at the guard and quietly treats it as a boolean, so the error shows up in the callers instead:
/**
* @param {unknown} value
* @returns {item is { sku: string }}
*/
export function isProduct(value) {
return typeof value === "object" && value !== null && "sku" in value;
}
/** @param {unknown} row */
export function label(row) {
return isProduct(row) ? row.sku : "unknown row";error TS18046: 'row' is of type 'unknown'.
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} Under TypeScript 6.0 and 7.0
Every example on this page also runs under TypeScript 6.0.3 and 7.0.2 when this site is built. One of them reports something different from 5.9.3, the version the page describes; the ✗ on its badge links to its entry below.
A type guard naming a missing parameter is reported
The pattern: @returns {item is Product} on a function whose parameter is value.
Why: 5.9 silently treats the function as returning boolean; TypeScript 7.0.2 reports TS1225 ("Cannot find parameter 'item'"), as a .ts file always did. The port's list of changes says JavaScript files are now analyzed like TypeScript files. Both still report the unnarrowed use (TS18046). Source: CHANGES.md: changes between TypeScript 6 and the native port.
A predicate that names a parameter the function does not have products.js
- TypeScript 5.9.3
products.js(12,27): error TS18046: 'row' is of type 'unknown'.
- TypeScript 6.0.3same as 5.9.3
The same errors as 5.9.3.
- TypeScript 7.0.2differs from 5.9.3
products.js(3,14): error TS1225: Cannot find parameter 'item'. products.js(12,27): error TS18046: 'row' is of type 'unknown'.
What works in every version: Name the parameter in the predicate: @returns {value is Product}.
The example A type guard with @returns {value is Product} on this page shows it, with the same result under every version.
Gotchas
- Check the predicate when you rename a parameter. A guard whose predicate names the old parameter stops narrowing without an error in JavaScript, as shown above.
falsenarrows too. In theelsebranch,rowis “not aProduct”, which matters for unions: a guard onProduct | ServiceleavesServicein theelse.this is …works in classes. A method can return@returns {this is PaidOrder}to narrow the object it is called on.
Questions
Does tsc check that my guard is right?
No. A type guard is a promise: tsc trusts value is Product whenever the function returns true. If the checks inside are wrong, the narrowing is wrong too, so keep guards small and test them.
What is the difference between a type guard and an assertion function?
A guard returns a boolean and narrows inside an if. An assertion function returns nothing and throws instead, and narrows everything after the call.
Sources
- Narrowing (using type predicates) · TypeScript handbook
- TypeScript 5.5 release notes (inferred type predicates) · TypeScript handbook
- JSDoc Reference (@returns) · TypeScript handbook
Last reviewed 21 October 2026.