Recipe · Objects and data

How do I write a type guard function in JSDoc?

  • Node.js (ES modules)
  • Type guards
  • @returns
  • unknown
  • Checked with TypeScript 5.9

The problem I wrote isProduct(value) to check unknown rows, but after if (isProduct(row)) tsc still treats row as unknown. How do I make my check function narrow the type?

The short answer

Return a boolean, and say what true means in the @returns tag: @returns {value is Product}:

products.js products.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
/**
 * @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";
}
A type guard with @returns {value is Product}.
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:

stock.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
/** @type {(number | undefined)[]} */
const counts = [4, undefined, 12];

const known = counts.filter((count) => count !== undefined);
console.log(known.map((count) => count.toFixed(0)));
Since TypeScript 5.5 a simple filter callback narrows on its own.
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):

products.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: products.js(12,30): error TS18046: 'row' is of type 'unknown'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. products.js(12,30): error TS18046: 'row' is of type 'unknown'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. products.js(12,30): error TS18046: 'row' is of type 'unknown'. checked 21 October 2026
/**
 * @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'.
}
The same check returning boolean narrows nothing.
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:

products.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: products.js(12,27): error TS18046: 'row' is of type 'unknown'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. products.js(12,27): error TS18046: 'row' is of type 'unknown'. 7.0.2 : differs, see why TypeScript 7.0.2, run 21 October 2026: differs 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'. checked 21 October 2026
/**
 * @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'.
}
A predicate that names a parameter the function does not have.
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

TypeScript 7.0 · JSDoc and JavaScript patterns 7.0 no longer reads

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.
  • false narrows too. In the else branch, row is “not a Product”, which matters for unions: a guard on Product | Service leaves Service in the else.
  • 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

Last reviewed 21 October 2026.