Type expression

Type predicates

  • Functions
  • Tested with TypeScript 5.9

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

catalog.js catalog.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 {{ 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();
A type predicate narrows, in if and in filter().
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}
catalog.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: catalog.js(15,36): error TS2339: Property 'sku' does not exist on type 'Product | Bundle'. Property 'sku' does not exist on type 'Bundle'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. catalog.js(15,36): error TS2339: Property 'sku' does not exist on type 'Product | Bundle'. Property 'sku' does not exist on type 'Bundle'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. catalog.js(15,36): error TS2339: Property 'sku' does not exist on type 'Bundle | Product'. Property 'sku' does not exist on type 'Bundle'. checked 21 October 2026
/** @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;
}
A function that returns boolean does not narrow.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}
catalog.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 {(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 };
Since TypeScript 5.5, simple checks are predicates on their own.
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 named item.
  • 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

Last reviewed 20 October 2026.