TypeScript 5.4 was announced on 6 March 2024. Its most useful change for JavaScript is in narrowing: a parameter or let that has been narrowed keeps its narrowed type inside a callback created after its last assignment, which removes many "possibly undefined" errors in code that uses map and forEach.
It also added the NoInfer utility type, types for Object.groupBy and Map.groupBy, and module: preserve, which models bundlers that accept both import and require().
At a glance
| Release | TypeScript 5.4 |
|---|---|
| Released | 6 March 2024, the day of its announcement on the TypeScript blog |
| Source | TypeScript 5.4 release notes |
| Examples | Every example runs through TypeScript 5.9.3 when this site is built. |
| Previous release | TypeScript 5.3 |
| Next release | TypeScript 5.5 |
What changed for JavaScript
Narrowing inside callbacks
After if (typeof tags === "string") tags = tags.split(","), tags is a string[], but a callback that read it used to see string | string[] again, because the callback might run later. Now TypeScript keeps the narrowing when the callback is created after the last assignment to the variable.
/**
* @param {string | string[]} tags
* @param {string[]} items
*/
export function label(tags, items) {
if (typeof tags === "string") {
tags = tags.split(",");
}
return items.map((item) => `${item} [${tags.join(", ")}]`);
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": [
"*.js"
]
} NoInfer
NoInfer<T> marks a parameter that should be checked against T without helping to infer it, so a default value cannot widen the type the other arguments set. It works in JSDoc like any utility type.
/**
* @template {string} T
* @param {T[]} sizes
* @param {NoInfer<T>} fallback
*/
export function pickSize(sizes, fallback) {
return sizes[0] ?? fallback;
}
pickSize(["S", "M", "L"], "M");
pickSize(["S", "M", "L"], "XL");error TS2345: Argument of type '"XL"' is not assignable to parameter of type '"S" | "M" | "L"'.jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} Object.groupBy and Map.groupBy
The types for the two grouping functions arrived in 5.4 under target: esnext, and moved to the es2024 lib in TypeScript 5.7. The groups of Object.groupBy are optional properties, so under strictNullChecks each one has to be checked before use.
module preserve
"module": "preserve" describes a bundler that keeps import and require() as written and resolves each through the matching exports condition. It implies moduleResolution: bundler, esModuleInterop and resolveJsonModule.
What did not change
A variable assigned inside a nested function is still not narrowed in callbacks, since that function might run at any time.
Upgrade notes
5.4 was the last release to accept the options deprecated in 5.0, with "ignoreDeprecations": "5.0"; 5.5 stops honoring them. The other breaking changes are mostly about conditional types, which JSDoc rarely uses, and enum, which a .js file cannot declare.
Gotchas
Object.groupByreturns an object with anullprototype, which TypeScript's type does not show;"toString" in groupsis false at run time.
Sources
- TypeScript 5.4 release notes · TypeScript handbook
- Announcing TypeScript 5.4 · TypeScript blog
Last reviewed 20 October 2026.