The short answer
Write @returns {asserts condition} for a helper that throws when its argument is falsy, or @returns {asserts value is string} for one that checks a type:
/**
* @param {unknown} condition
* @param {string} message
* @returns {asserts condition}
*/
export function assert(condition, message) {
if (!condition) throw new Error(message);
}
/**
* @param {unknown} value
* @param {string} name
* @returns {asserts value is string}
*/
export function assertString(value, name) {
if (typeof value !== "string") throw new TypeError(`${name} must be a string`);
}
/** @param {{ id?: string, note: unknown }} order */
export function printOrder(order) {
assert(order.id, "order has no id");
assertString(order.note, "note");
console.log(order.id.toUpperCase(), order.note.trim());
}export function assert(condition: unknown, message: string): asserts condition {
if (!condition) throw new Error(message);
}
export function assertString(value: unknown, name: string): asserts value is string {
if (typeof value !== "string") throw new TypeError(`${name} must be a string`);
}
export function printOrder(order: { id?: string; note: unknown }) {
assert(order.id, "order has no id");
assertString(order.note, "note");
console.log(order.id.toUpperCase(), order.note.trim());
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} After assert(order.id, …), order.id is a string, not string | undefined; after assertString(order.note, …), order.note is a string, not unknown. The narrowing lasts for the rest of the function.
Why it works
An assertion function returns normally only when its check passes; otherwise it throws. asserts condition tells tsc exactly that, so the code after the call is analyzed as if the condition were true. asserts value is string combines it with a type predicate: if the call returns, the argument is a string.
The same thing in TypeScript
The TypeScript equivalent tab writes the same text as the return type: function assert(condition: unknown, message: string): asserts condition. Assertion functions return nothing, so the asserts clause takes the place of void.
What goes wrong
For tsc to use an assertion, it must know the function’s type where it is declared, without inferring it. A function declaration qualifies; a variable that holds the result of a factory does not, and the call fails with TS2775, after which nothing is narrowed (TS18046):
/** @param {string} name */
function makeStringAssert(name) {
/**
* @param {unknown} value
* @returns {asserts value is string}
*/
return (value) => {
if (typeof value !== "string") throw new TypeError(`${name} must be a string`);
};
}
const assertNote = makeStringAssert("note");
/** @param {unknown} note */
export function printNote(note) {
assertNote(note);error TS2775: Assertions require every name in the call target to be declared with an explicit type annotation.
console.log(note.trim());error TS18046: 'note' is of type 'unknown'.
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} Give the variable a type with @type and the same call works:
/** @param {string} name */
function makeStringAssert(name) {
/**
* @param {unknown} value
* @returns {asserts value is string}
*/
return (value) => {
if (typeof value !== "string") throw new TypeError(`${name} must be a string`);
};
}
/** @type {(value: unknown) => asserts value is string} */
const assertNote = makeStringAssert("note");
/** @param {unknown} note */
export function printNote(note) {
assertNote(note);
console.log(note.trim());
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} Gotchas
- Every name in the call needs a declared type.
asserts.string(note)on an untypedconst asserts = { … }object is TS2775 too; give the object a type, or export plain functions. - A
constwith JSDoc on its arrow function counts./** @param … @returns {asserts value is string} */ const assertString = (value) => …works, because the JSDoc declares the function’s type. - Assertions do not return values. A function with
assertsin its@returnscannot return anything else; return values belong to type guards.
Questions
Can I use Node.js's own assert module?
Yes. @types/node declares assert(value) and ok(value) as assertion functions, so after import assert from "node:assert" a call like assert(order.id) narrows the same way.
Which TypeScript version added assertion functions?
TypeScript 3.7 added them. Its release notes show only the TypeScript syntax, so this page claims the JSDoc form only for the version its examples run on, TypeScript 5.9.
Sources
- TypeScript 3.7 release notes (assertion functions) · TypeScript handbook
- JSDoc Reference (@returns) · TypeScript handbook
- assert(value[, message]) · Node.js documentation
Last reviewed 21 October 2026.