TypeScript release

TypeScript 5.4 for JavaScript users

  • Released 6 March 2024
  • Examples run with TypeScript 5.9

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

ReleaseTypeScript 5.4
Released6 March 2024, the day of its announcement on the TypeScript blog
SourceTypeScript 5.4 release notes
ExamplesEvery example runs through TypeScript 5.9.3 when this site is built.
Previous releaseTypeScript 5.3
Next releaseTypeScript 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.

labels.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
/**
 * @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(", ")}]`);
}
After its last assignment, a narrowed parameter stays narrowed in a callback.
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.

pick.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: pick.js(12,27): error TS2345: Argument of type '"XL"' is not assignable to parameter of type '"S" | "M" | "L"'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. pick.js(12,27): error TS2345: Argument of type '"XL"' is not assignable to parameter of type '"S" | "M" | "L"'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. pick.js(12,27): error TS2345: Argument of type '"XL"' is not assignable to parameter of type '"L" | "M" | "S"'. checked 21 October 2026
/**
 * @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"'.
NoInfer<T>: infer from one argument only.
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.groupBy returns an object with a null prototype, which TypeScript's type does not show; "toString" in groups is false at run time.

Sources

Last reviewed 20 October 2026.