TypeScript 4.9 was announced on 15 November 2022. Its headline feature, the satisfies operator, is TypeScript syntax; JavaScript files got the same check as the @satisfies tag one release later, in 5.0.
What a .js file does get: the in operator narrows an object to one that has the property, which makes unknown JSON easier to check; an equality comparison with NaN is now an error; and when TypeScript writes JavaScript output from .js files, it keeps every import.
At a glance
| Release | TypeScript 4.9 |
|---|---|
| Released | 15 November 2022, the day of its announcement on the TypeScript blog |
| Source | TypeScript 4.9 release notes |
| Examples | Every example runs through TypeScript 5.9.3 when this site is built. |
| Previous release | TypeScript 4.8 |
| Next release | TypeScript 5.0 |
What changed for JavaScript
The in operator narrows to an object with that property
Before 4.9, "name" in data only narrowed between types that already listed name. Now it also narrows a type that does not mention the property: after the check, data is known to have a name of type unknown, which you can then test with typeof. That is the usual way to check the result of JSON.parse, which TypeScript types as any; give it an unknown type first and the checks below are required.
/** @param {unknown} data */
export function packageName(data) {
if (data && typeof data === "object" && "name" in data && typeof data.name === "string") {
return data.name;
}
return undefined;
}
console.log(packageName(JSON.parse('{"name":"left-pad"}')));jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": [
"*.js"
]
} Equality checks against not-a-number
Nothing is equal to NaN, not even NaN, so value === NaN is always false. TypeScript 4.9 reports such comparisons (TS2845) and suggests Number.isNaN(value).
/** @param {string} input */
export function parsePrice(input) {
const value = Number(input);
if (value === NaN) {error TS2845: This condition will always return 'false'.
return null;
}
return Number.isNaN(value) ? null : value;
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": [
"*.js"
]
} JavaScript output keeps every import
When TypeScript writes .js output from .js input (allowJs with an outDir), older versions dropped an import that was only used in JSDoc, such as a class named in a @type tag. 4.9 keeps imports in JavaScript files exactly as written. A project that only checks its files (noEmit, the jsconfig.json default) is not affected.
satisfies is TypeScript syntax
The new operator checks a value against a type without changing the value's own type. Written in a .js file, it is a syntax error; the JSDoc form, /** @satisfies {Type} */, arrived in TypeScript 5.0 and is covered on the @satisfies page.
/** @typedef {{ theme: "light" | "dark", pageSize: number }} Settings */
const settings = { theme: "dark", pageSize: 24 } satisfies Settings;error TS8037: Type satisfaction expressions can only be used in TypeScript files.
export { settings };jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} Watch mode uses file system events
tsc --watch now waits for the operating system to report a change instead of polling files, and falls back to polling only where events are not available. The release notes describe the watchOptions settings for the cases where events misbehave.
What did not change
JSDoc tags and the way TypeScript reads them are the same as in 4.8. A project that only checks its files sees no difference from the emit change.
- The
satisfieskeyword cannot be written in a.jsfile; use the tag from 5.0. - The 4.8 rule still holds: a type-only name cannot be imported by name in a
.jsfile.
Upgrade notes
From the release notes (the check for NaN is listed as a new feature, the others as breaking changes); the first two can produce new errors in checked JavaScript.
- Comparisons with
NaNare errors (TS2845). Promise.resolvenow unwraps nested promises withAwaited, so code that relied on gettinganyback may see a more precise type.- If you write JavaScript output from
.jsfiles, check that imports used only in JSDoc are safe to keep at run time. - Under
moduleResolution: node16, a package'sexportsnow wins over itstypesVersions; this matters to library authors who publish types for several TypeScript versions.
Gotchas
Number.isNaN(x)andx !== xare the two checks that work; the globalisNaN()converts its argument first, soisNaN("abc")istrue.
Sources
- TypeScript 4.9 release notes · TypeScript handbook
- Announcing TypeScript 4.9 · TypeScript blog
Last reviewed 20 October 2026.