Checked JavaScript mostly behaves the way the JSDoc reference says it will. This page collects the places where it does not: rules about which comment belongs to which code, tags that quietly do less than their names suggest, types that are looser in a .js file than in a .ts file, and older JSDoc syntax that means something else to TypeScript. Every item has an example that this site checks with TypeScript 5.9 on every build, so if a release changes the behavior, this page fails its build before it goes stale. Each item links the page that explains it in full.
The examples run with strict on unless their config says otherwise.
Comments that attach to the wrong thing
Only /** comments are JSDoc
A comment that starts with /* and one star is an ordinary comment, however many tags it holds. Nothing reports the mistake; the parameter is simply untyped, which strict turns into TS7006:
/* @param {string} title */
export function slugify(title) {error TS7006: Parameter 'title' implicitly has an 'any' type.
return title.toLowerCase().replace(/\s+/g, "-");
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["**/*.js"]
} Only the last comment above a declaration is read
Two JSDoc comments in a row, one for the description and one for the tags, look tidy and lose the tags. Put everything in one comment. (How TypeScript reads a JavaScript file)
/** @param {number[]} prices */
/** Adds up a list of prices. */
export function total(prices) {error TS7006: Parameter 'prices' implicitly has an 'any' type.
let sum = 0;
for (const price of prices) sum += price;
return sum;
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["**/*.js"]
} A comment on the same line as an opening brace is not attached
A JSDoc comment written after the { of a class, on the same line, belongs to no member. The same holds for the opening brace of an object literal. Start the comment on its own line:
export class Cart { /** @param {number} cents */
add(cents) {error TS7006: Parameter 'cents' implicitly has an 'any' type.
return cents;
}
}
export class Wishlist {
/** @param {string} sku */
add(sku) {
return sku;
}
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["**/*.js"]
} A statement in between detaches the comment
The comment belongs to the next declaration, whatever it is. A constant moved between the comment and its function takes the comment with it, and nothing says so (JSDoc style guide):
/** @param {number} cents */
const CURRENCY = "USD";
export function formatCents(cents) {error TS7006: Parameter 'cents' implicitly has an 'any' type.
return `${(cents / 100).toFixed(2)} ${CURRENCY}`;
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["**/*.js"]
} An email address without angle brackets starts a new tag
Any @ inside a JSDoc comment starts a tag, so jane@example.com in an @author line becomes an author “Jane Doe jane” and an unknown tag. It changes no types, but hovers read oddly. Write <jane@example.com> (@author):
// Hover normalizeCode: TypeScript shows an author tag reading "Jane Doe jane"
// and an example tag reading ".com", because the email is not in angle brackets.
/**
* Lowercases a product code.
*
* @author Jane Doe jane@example.com
* @param {string} code
*/
export function normalizeCode(code) {
return code.toLowerCase();
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} Tags that do less than they say
@readonly inside a @typedef ends the property list
A @readonly line among @property lines does not mark the next property read-only. It closes the type, and every property after it disappears (@readonly). Write readonly inside an object type instead: @typedef {{ readonly id: string, total: number }} Order.
/**
* @typedef {object} Order
* @property {string} id
* @readonly
* @property {number} total
*/
/** @type {Order} */
const order = { id: "A-100", total: 42 };error TS2353: Object literal may only specify known properties, and 'total' does not exist in type 'Order'.
export { order };jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} @property on a class comment is ignored
@property belongs to @typedef and @callback. On a class it declares nothing, so the property does not exist (@property); declare a field in the class body or assign it in the constructor:
/**
* @property {string} sku
*/
export class Product {}
new Product().sku;error TS2339: Property 'sku' does not exist on type 'Product'.jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} @extends without an extends clause is ignored
@extends gives type arguments to a base class the code already extends. On a class with no extends clause it does nothing, silently (@extends):
class Counter {
count = 0;
}
/** @extends {Counter} */
export class Tally {
bump() {
this.count += 1;error TS2339: Property 'count' does not exist on type 'Tally'.
}
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["**/*.js"]
} The opposite mistake is silent too: extend a generic class such as Map without @extends, and its type parameters are any:
export class Tally extends Map {
/** @param {string} key */
bump(key) {
this.set(key, (this.get(key) ?? 0) + 1);
return this;
}
}
// No error: the base class is Map<any, any>, so a number key and a string value pass.
new Tally().set(1, "one");jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} @private in a constructor function points at the wrong line
@private works on class members. In an old-style constructor function, TypeScript 5.9 reports TS2341 on the line that creates the property, inside the constructor, rather than where it is used from outside (@private). TypeScript 7.0 no longer reads constructor functions as classes at all; write a class with a #private field.
/** @constructor */
export function Account() {
/** @private */
this.balance = 0;error TS2341: Property 'balance' is private and only accessible within class 'Account'.
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} @enum checks the member type, not the members
@enum {string} types the object’s values as string, so any string passes where the enum is expected (@enum). A @typedef of a union of the values checks them, and TypeScript 7.0 drops @enum anyway:
/** @enum {string} */
export const Status = {
Draft: "draft",
Live: "live",
};
/** @param {Status} status */
export function setStatus(status) {
return status;
}
// Status means string here, so a value that is not in the enum still passes.
setStatus("deleted");jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} @callback without @returns returns any
A callback type with no @returns does not return void; it returns any, so whatever a caller does with the result goes unchecked (@callback):
/**
* @callback Compare
* @param {string} a
* @param {string} b
*/
/** @type {Compare} */
const byLength = (a, b) => a.length - b.length;
const result = byLength("tea", "coffee");
result.toUpperCase();
export { byLength };jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["**/*.js"]
} A misspelled name in @import is not reported
A name in an @import comment is resolved only when a tag uses it. A typo that nothing uses yet stays silent, and surfaces later in an unrelated pull request:
/** @import { Ordr } from "./types.js" */
/** @param {number} total */
export function report(total) {
return `Total: ${total}`;
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["**/*.js"]
} @this on an arrow function is an error
Arrow functions take this from where they are written, so @this cannot apply. TypeScript reports TS2730 on the tag. Use a function expression when the handler needs this:
/** @this {HTMLButtonElement} */error TS2730: An arrow function cannot have a 'this' parameter.
export const onClick = () => {
console.log("clicked");
};jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["**/*.js"]
} @type on a function types the whole signature
On a function, @type gives the entire function a type, return type included. An Express handler typed with RequestHandler therefore has to return what that type says, and a block body without return is TS2355 in a .js file, where the same handler passes in a .ts file (Express recipe). @satisfies or @param tags avoid it:
import express from "express";
const app = express();
/** @type {import("express").RequestHandler<{ id: string }>} */
const getUser = (req, res) => {error TS2355: A function whose declared type is neither 'undefined', 'void', nor 'any' must return a value.
res.json({ id: req.params.id });
};
app.get("/users/:id", getUser);jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": ["node"]
},
"include": ["*.js"]
} Types that are looser in a .js file
A value’s name works as a type
In a .js file, {createCart} in a type position means typeof createCart, so ReturnType<createCart> passes; the same line in a .ts file is TS2749, and TypeScript 7.0 rejects it in both. Write typeof (ReturnType):
export function createCart() {
return { lines: /** @type {string[]} */ ([]) };
}
// TypeScript resolves a value name in JSDoc to the value's type, so this passes in a .js file.
/** @type {ReturnType<createCart>} */
const cart = createCart();
export { cart };export function createCart() {
return { lines: [] as string[] };
}
const cart: ReturnType<createCart> = createCart();error TS2749: 'createCart' refers to a value, but is being used as a type here. Did you mean 'typeof createCart'?
export { cart };jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} Type parameters with nothing to infer from are any
A generic function called without anything to infer its type parameter from returns any in a .js file, where a .ts file would use unknown (generic inference). Pass a typed argument, or give the result a @type:
/**
* @template T
* @returns {{ items: T[] }}
*/
export function createStore() {
return { items: [] };
}
// T has no inference source here, so in a .js file it becomes any: nothing is checked.
const loose = createStore();
loose.items.push(1, "two");
/** @type {{ items: number[] }} */
const counts = createStore();
counts.items.push(1);
counts.items.push("two");error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
export { loose, counts };jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} Without noImplicitAny, an untyped object accepts any property
In a .js file, an object literal without a declared type is open-ended: code may add properties to it later. While noImplicitAny is off, that also means reading a property it never had is not an error, so order.totalCent (for totalCents) passes. With noImplicitAny on, as under strict, the same read is TS2551 (choosing a strictness level):
const orders = [{ id: "A-100", totalCents: 4250 }];
export function totalOf(id) {
const order = orders.find((o) => o.id === id);
return order.totalCent;
}
totalOf(100);jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": false,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["**/*.js"]
} An empty array literal accepts anything
const ids = [] starts as an array of any in a .js file, and every push is accepted. Give it a type where it is declared (arrays):
// In a .js file an empty array literal starts as any[], so nothing below is checked.
const ids = [];
ids.push(100);
ids.push("A-101");
/** @type {number[]} */
const orderNumbers = [];
orderNumbers.push(100);
orderNumbers.push("A-101");error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
export { ids, orderNumbers };jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} Untyped parameters are optional
A parameter without a type can be left out by callers; one with a @param type cannot. The rest parameters page shows the related rule: a function that reads arguments accepts any number of them.
function greet(name, greeting) {
return (greeting || "Hello") + ", " + (name || "you");
}
/**
* @param {string} name
* @param {string} greeting
*/
function greetTyped(name, greeting) {
return greeting + ", " + name;
}
greet();
greet("Ada");
greetTyped("Ada");error TS2554: Expected 2 arguments, but got 1.jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": false,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["**/*.js"]
} A cast checks almost nothing
/** @type {Settings} */ (value) accepts an object that is missing a required property: a cast states a type, it does not check one (annotate, cast or satisfy). Use @type on the declaration or @satisfies to get the check:
/** @typedef {{ theme: "light" | "dark", pageSize: number }} Settings */
/** @type {Settings} */
const annotated = { theme: "dark", pageSize: 24 };
// The cast is accepted although pageSize is missing: a cast claims, it does not check.
const cast = /** @type {Settings} */ ({ theme: "dark" });
/** @satisfies {Settings} */
const checked = { theme: "dark", pageSize: 24 };
/** @type {"dark"} */
const fromChecked = checked.theme;
/** @type {"dark"} */
const fromAnnotated = annotated.theme;error TS2322: Type '"light" | "dark"' is not assignable to type '"dark"'.
Type '"light"' is not assignable to type '"dark"'.
export { cast, fromChecked, fromAnnotated };jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} Omit does not check its keys
Pick reports a key the type does not have; Omit accepts any key, so a misspelled one removes nothing and nothing says so (Omit). This is TypeScript’s own definition, the same in .ts files:
/** @typedef {{ id: string, sku: string, price: number }} Product */
// No error: Omit accepts any key, so the typo removes nothing.
/** @typedef {Omit<Product, "iid">} Draft */
/** @type {Draft} */
const draft = { id: "p_1", sku: "TEE-01", price: 19.5 };
export { draft };jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} A type predicate that names a missing parameter is a plain boolean
@returns {item is Product} on a function whose parameter is called value is not an error in a .js file (in .ts it is TS1225); the function just returns boolean, and nothing narrows (type guards):
/**
* @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"]
} A function’s properties take every type assigned to them
Properties assigned to a function after its declaration get the union of every value assigned anywhere in the file, so a later assignment of a number widens a string property (functions with properties):
/**
* @param {number} cents
* @returns {string}
*/
export function formatPrice(cents) {
return `${formatPrice.symbol.trim()}${(cents / 100).toFixed(2)}`;error TS2339: Property 'trim' does not exist on type 'string | number'.
Property 'trim' does not exist on type 'number'.
}
formatPrice.symbol = "$";
formatPrice.symbol = 36;
console.log(formatPrice.currency);error TS2339: Property 'currency' does not exist on type 'typeof formatPrice'.jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} Syntax that means something else
first<string>(x) is two comparisons
JavaScript has no type arguments, so in a .js file first<string>(rows) parses as first < string > (rows): TS2365 for the comparison and TS2693 for using a type as a value. Type the argument or the result instead:
/**
* @template T
* @param {T[]} items
* @returns {T | undefined}
*/
export function first(items) {
return items[0];
}
/** @type {unknown[]} */
const rows = ["TEE-01"];
const sku = first<string>(rows);error TS2365: Operator '>' cannot be applied to types 'boolean' and 'unknown[]'.error TS2693: 'string' only refers to a type, but is being used as a value here.
export { sku };jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} A tuple in a rest @param types each argument
@param {...[string, number]} entries means “any number of [string, number] pairs”, not “a string, then a number”. To type the argument list as a whole, give the rest parameter a tuple type without the dots (rest parameters):
/** @param {...[string, number]} entries */
export function addStock(...entries) {
return entries.length;
}
addStock(["TEA-01", 3], ["CUP-02", 1]);
addStock("TEA-01", 3);error TS2345: Argument of type 'string' is not assignable to parameter of type '[string, number]'.
/** @param {[string, ...number[]]} args */
export function receive(...args) {
return args.length;
}
receive("TEA-01", 3, 4);jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["**/*.js"]
} A postfix = in an object type is a syntax error
Closure Compiler marked optional parameters with a trailing =. Inside an object type it is a JSDoc syntax error, and the property stays required (object shapes). Write unit?: string:
/** @type {{ street: string, unit: string= }} */error TS1247: A type literal property cannot have an initializer.error TS1109: Expression expected.
const address = { street: "12 Market St" };error TS2741: Property 'unit' is missing in type '{ street: string; }' but required in type '{ street: string; unit: string; }'.
export { address };jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} !Type does not remove null
In Closure Compiler, {!HTMLElement} meant “never null”. TypeScript parses it as HTMLElement and leaves the null that querySelector can return in place (Closure syntax):
/** @type {!HTMLElement} */
const link = document.querySelector("a");error TS2322: Type 'HTMLAnchorElement | null' is not assignable to type 'HTMLElement'.
Type 'null' is not assignable to type 'HTMLElement'.
export { link };jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} Lowercase array and promise are not types
Old JSDoc often wrote {array} and {promise}. With noImplicitAny they are unknown names (TS2552); write string[] and Promise<T>:
/** @type {Array<string>} */
const tags = [];
/** @type {array} */error TS2552: Cannot find name 'array'. Did you mean 'Array'?
const legacyTags = [];
/** @param {promise} pending */error TS2552: Cannot find name 'promise'. Did you mean 'Promise'?
export function settle(pending) {
return pending;
}
export { tags, legacyTags };jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} Behavior that changes in TypeScript 7.0
Every example on this page also runs under TypeScript 6.0 and 7.0 when the site is built; the ones that report something different are explained in the next section, from the results of that run. The TypeScript 7.0 page lists what the native port no longer reads in JavaScript files, and several quirks above go with it: a value’s name as a type, @enum, constructor functions (and so @private inside them), Closure function types and the extra arguments a function that reads arguments accepted. Two silent ones are reported by 7.0: a misspelled @import name nothing uses and a type guard that names a missing parameter; and a function typed with @type is no longer checked for a missing return. In each case the form this site recommends (typeof, a union @typedef, class, arrow function types, a rest parameter, @satisfies) passes in 5.9, 6.0 and 7.0 alike; TypeScript 5.9 vs 6.0 vs 7.0 has the upgrade path and every difference class.
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. Six of them report something different from 5.9.3, the version the page describes; the ✗ on their badges link to their entries below.
@enum no longer declares a type
The pattern: An object marked @enum and its name used as a type (@param {Status} status), or relied on to check its values.
Why: The port's list of changes drops Closure-only JSDoc, @enum among it: the tag is ordinary, so the object's name used as a type is a value used as a type (TS2749), and the values are no longer checked against the @enum type (the TS2322 5.9 reported is gone). Source: CHANGES.md: Values are no longer resolved as types in JSDoc type positions.
An @enum type accepts any value of its member type status.js
- TypeScript 5.9.3
No errors.
- TypeScript 6.0.3same as 5.9.3
No errors.
- TypeScript 7.0.2differs from 5.9.3
status.js(7,13): error TS2749: 'Status' refers to a value, but is being used as a type here. Did you mean 'typeof Status'?
What works in every version: A const object with @type {const} and a @typedef of its values, which checks the values more tightly in every version.
export const Status = /** @type {const} */ ({
Draft: "draft",
Live: "live",
});
/** @typedef {(typeof Status)[keyof typeof Status]} StatusValue */
/** @param {StatusValue} status */
export function setStatus(status) {
return status;
}
setStatus(Status.Live);
setStatus("deleted");error TS2345: Argument of type '"deleted"' is not assignable to parameter of type 'StatusValue'.export const Status = {
Draft: "draft",
Live: "live",
} as const;
type StatusValue = (typeof Status)[keyof typeof Status];
export function setStatus(status: StatusValue) {
return status;
}
setStatus(Status.Live);
setStatus("deleted");error TS2345: Argument of type '"deleted"' is not assignable to parameter of type 'StatusValue'.jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} Constructor functions are not classes
The pattern: A function that sets this.name = … and is called with new, with or without @class or @constructor, and methods added through prototype.
Why: The port's list of changes says constructor functions are no longer supported and @class or @constructor no longer makes a function a constructor. So this in the function is untyped (TS2683 under strict), new on it is untyped (TS7009), and the checks that came with the class (TS2348 for a call without new, TS2341 for a @private property) are gone. Source: CHANGES.md: Constructor functions are no longer supported.
@private in a constructor function reports the wrong line account.js
- TypeScript 5.9.3
account.js(5,8): error TS2341: Property 'balance' is private and only accessible within class 'Account'.
- TypeScript 6.0.3same as 5.9.3
The same errors as 5.9.3.
- TypeScript 7.0.2differs from 5.9.3
account.js(5,3): error TS2683: 'this' implicitly has type 'any' because it does not have a type annotation.
What works in every version: Write a class, with fields and methods in its body.
/** @typedef {{ sku: string, quantity: number }} Line */
export class Cart {
/** @type {Line[]} */
items = [];
/** @type {string | null} */
coupon = null;
/**
* @param {string} currency ISO code, such as "USD".
*/
constructor(currency) {
this.currency = currency;
}
/**
* @param {string} sku
* @param {number} [quantity]
*/
add(sku, quantity = 1) {
this.items.push({ sku, quantity });
return this;
}
}
const cart = new Cart("USD").add("LAMP-01").add("DESK-02", 2);
console.log(cart.currency.toLowerCase(), cart.items.length, cart.coupon?.trim());type Line = { sku: string; quantity: number };
export class Cart {
items: Line[] = [];
coupon: string | null = null;
currency: string;
constructor(currency: string) {
this.currency = currency;
}
add(sku: string, quantity = 1): this {
this.items.push({ sku, quantity });
return this;
}
}
const cart = new Cart("USD").add("LAMP-01").add("DESK-02", 2);
console.log(cart.currency.toLowerCase(), cart.items.length, cart.coupon?.trim());jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} A value's name is not a type
The pattern: {ReturnType<createCart>} or {Status}, where createCart or Status is a value, not a type.
Why: 5.9 read a value's name in a JSDoc type as typeof that value, in .js files only. The port's list of changes says values are no longer resolved as types in JSDoc, so the .js file now reports TS2749, as its .ts twin always did. Source: CHANGES.md: Values are no longer resolved as types in JSDoc type positions.
In a .js file a function name also works as a type; in .ts it needs typeof cart.js
- TypeScript 5.9.3
cart.ts(6,24): error TS2749: 'createCart' refers to a value, but is being used as a type here. Did you mean 'typeof createCart'?
- TypeScript 6.0.3same as 5.9.3
The same errors as 5.9.3.
- TypeScript 7.0.2differs from 5.9.3
cart.js(6,23): error TS2749: 'createCart' refers to a value, but is being used as a type here. Did you mean 'typeof createCart'? cart.ts(6,24): error TS2749: 'createCart' refers to a value, but is being used as a type here. Did you mean 'typeof createCart'?
What works in every version: Write typeof: {ReturnType<typeof createCart>}.
export function createCart() {
return {
/** @type {{ sku: string, quantity: number }[]} */
lines: [],
coupon: /** @type {string | null} */ (null),
};
}
/** @typedef {ReturnType<typeof createCart>} Cart */
/** @param {Cart} cart */
export function itemCount(cart) {
return cart.lines.reduce((n, line) => n + line.quantity, 0);
}
itemCount(createCart());export function createCart() {
return {
lines: [] as { sku: string; quantity: number }[],
coupon: null as string | null,
};
}
type Cart = ReturnType<typeof createCart>;
export function itemCount(cart: Cart) {
return cart.lines.reduce((n, line) => n + line.quantity, 0);
}
itemCount(createCart());jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} A misspelled @import name is reported even when unused
The pattern: /** @import { Ordr } from "./types.js" */ for a type named Order, with Ordr never used.
Why: 5.9 checks an @import name only where it is used, so an unused typo passes; TypeScript 7.0.2 checks the tag itself and reports TS2724 ("Did you mean 'Order'?"). The port's list of changes says JavaScript files are now analyzed like TypeScript files, where an unknown name in an import is always an error. Source: CHANGES.md: changes between TypeScript 6 and the native port.
A misspelled name in @import is not reported until it is used report.js
- TypeScript 5.9.3
No errors.
- TypeScript 6.0.3same as 5.9.3
No errors.
- TypeScript 7.0.2differs from 5.9.3
report.js(1,15): error TS2724: '"./types.js"' has no exported member named 'Ordr'. Did you mean 'Order'?
What works in every version: Fix the name, or remove the import if nothing uses it.
import { orderTotal } from "./orders.js";
/** @import { Order, LineItem } from "./types.js" */
/** @type {LineItem} */
const lamp = { sku: "LAMP-01", cents: 3900 };
/** @type {Order} */
const order = { id: "A-1001", lines: [lamp] };
console.log(orderTotal(order));import { orderTotal } from "./orders.js";
import type { Order, LineItem } from "./types.js";
const lamp: LineItem = { sku: "LAMP-01", cents: 3900 };
const order: Order = { id: "A-1001", lines: [lamp] };
console.log(orderTotal(order));jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} A function typed with @type is not checked for a missing return
The pattern: /** @type {RequestHandler} */ on an arrow function that returns nothing, where the handler type's return type is not void.
Why: 5.9 reports TS2355 ("must return a value") on the arrow function; TypeScript 7.0.2 does not. The port's list of changes does not mention it. Express ignores what a handler returns, so the code itself is fine. Source: CHANGES.md: changes between TypeScript 6 and the native port.
@type on a handler that returns nothing server.js
- TypeScript 5.9.3
server.js(7,17): error TS2355: A function whose declared type is neither 'undefined', 'void', nor 'any' must return a value.
- TypeScript 6.0.3same as 5.9.3
The same errors as 5.9.3.
- TypeScript 7.0.2differs from 5.9.3
No errors.
What works in every version: Use @satisfies instead of @type: the handler is checked against the type and keeps its own return type, the same in every version.
import express from "express";
const app = express();
/** @satisfies {import("express").RequestHandler<{ id: string }>} */
const getUser = (req, res) => {
res.json({ id: req.params.id, name: "Ada" });
};
app.get("/users/:id", getUser);
app.get("/orders/:orderId", (req, res) => {
res.json({ orderId: req.params.orderId });
});
app.listen(3000);import express, { type RequestHandler } from "express";
const app = express();
const getUser = ((req, res) => {
res.json({ id: req.params.id, name: "Ada" });
}) satisfies RequestHandler<{ id: string }>;
app.get("/users/:id", getUser);
app.get("/orders/:orderId", (req, res) => {
res.json({ orderId: req.params.orderId });
});
app.listen(3000);jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": ["node"]
},
"include": ["*.js"]
} 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}.
/**
* @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"]
} Questions
Are these bugs in TypeScript?
Mostly not. Several are compatibility rules for older JavaScript (open objects, optional parameters, values used as types), and TypeScript 7.0 removes a number of them. A few are JSDoc parsing rules that are easy to trip over. Each one is shown here as TypeScript 5.9 behaves, so you can recognize it.
How do I find these in my own code?
Most of them surface as soon as strict is on, because a comment that attaches to nothing leaves an untyped parameter (TS7006). The silent ones (open objects, @enum values, Omit keys, casts) need a @type or @satisfies in the right place; the sections below say which.
Sources
- JSDoc Reference · TypeScript handbook
- Type Checking JavaScript Files · TypeScript handbook
- CHANGES.md: changes between TypeScript 6 and the native port · microsoft/typescript-go on GitHub
- TypeScript 4.2: type arguments in JavaScript · TypeScript handbook
Last reviewed 21 October 2026.