TypeScript release

TypeScript 5.9 for JavaScript users

  • Released 1 August 2025
  • Examples run with TypeScript 5.9

TypeScript 5.9 was announced on 1 August 2025, and it is the version this site checks every example with. For JavaScript projects it brought a much shorter tsc --init, the import defer syntax, a stable module: node20 setting and longer, more useful hovers in editors.

Its main breaking change is in the built-in types: ArrayBuffer is no longer a supertype of the typed arrays, so a Uint8Array or a Node.js Buffer passed where an ArrayBuffer is expected is now an error.

At a glance

ReleaseTypeScript 5.9
Released1 August 2025, the day of its announcement on the TypeScript blog
SourceTypeScript 5.9 release notes
ExamplesEvery example runs through TypeScript 5.9.3 when this site is built.
Previous releaseTypeScript 5.8
Next releaseTypeScript 6.0

What changed for JavaScript

A shorter tsc --init

tsc --init writes a short tsconfig.json with current settings instead of a long commented list: module: nodenext, target: esnext, types: [], strict and several stricter checks. It does not set allowJs or checkJs; this site's quick start shows how to turn its output into a jsconfig.json.

lines.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
import { readFile } from "node:fs/promises";

/**
 * @param {string} path
 * @returns {Promise<string[]>}
 */
export async function readLines(path) {
  const text = await readFile(path, "utf8");
  return text.split("\n");
}

const [first] = await readLines("notes.txt");
// With noUncheckedIndexedAccess, `first` is string | undefined.
console.log(first?.trim());
A script checked with the config tsc --init writes, renamed to jsconfig.json.
jsconfig.json for this example
{
  // Visit https://aka.ms/tsconfig to read more about this file
  "compilerOptions": {
    // File Layout
    // "rootDir": "./src",
    // "outDir": "./dist",

    // Environment Settings
    // See also https://aka.ms/tsconfig/module
    "module": "nodenext",
    "target": "es2024",
    "types": ["node"],
    // For nodejs:
    // "lib": ["esnext"],
    // "types": ["node"],
    // and npm install -D @types/node

    // Other Outputs
    "sourceMap": true,
    "declaration": true,
    "declarationMap": true,

    // Stricter Typechecking Options
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,

    // Style Options
    // "noImplicitReturns": true,
    // "noImplicitOverride": true,
    // "noUnusedLocals": true,
    // "noUnusedParameters": true,
    // "noFallthroughCasesInSwitch": true,
    // "noPropertyAccessFromIndexSignature": true,

    // Recommended Options
    "strict": true,
    "jsx": "react-jsx",
    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "noUncheckedSideEffectImports": true,
    "moduleDetection": "force",
    "skipLibCheck": true,

    "allowJs": true,
    "checkJs": true,
    "noEmit": true,
  }
}

import defer

import defer * as report from "./report.js" loads a module without running it; its code runs the first time one of its exports is read. Only namespace imports can be deferred, and TypeScript accepts the syntax only with module: preserve or esnext, because it does not rewrite it. Your runtime or bundler has to support it.

main.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
import defer * as report from "./report.js";

/** @param {number[]} values */
export function total(values) {
  // report.js runs here, the first time sum is read
  return report.sum(values);
}
import defer: the module runs on first use.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "esnext",
    "types": [],
    "moduleResolution": "bundler"
  },
  "include": [
    "*.js"
  ]
}

module node20

"module": "node20" models Node.js 20 and, unlike nodenext, will not change in later releases. It implies target: es2023, where nodenext implies esnext.

Longer hovers and DOM summaries

Hovers on DOM APIs now include a short summary from MDN, hovers can be expanded to show the types inside a type (a preview in VS Code), and the maximum hover length is larger and configurable (js/ts.hover.maximumLength in VS Code). All of it works in .js files, where hovers are how you read the types JSDoc gives your code.

A Uint8Array is not an ArrayBuffer

The built-in types changed so that ArrayBuffer is no longer a supertype of Uint8Array and the other typed arrays, nor of Node.js Buffer. Code that passes a typed array where an ArrayBuffer is declared now fails (TS2345). Pass bytes.buffer, or declare the parameter as Uint8Array. Errors that mention Buffer usually mean @types/node needs updating.

bytes.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: bytes.js(8,32): error TS2345: Argument of type 'Uint8Array<ArrayBuffer>' is not assignable to parameter of type 'ArrayBuffer'. Types of property '[Symbol.toStringTag]' are incompatible. Type '"Uint8Array"' is not assignable to type '"ArrayBuffer"'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. bytes.js(8,32): error TS2345: Argument of type 'Uint8Array<ArrayBuffer>' is not assignable to parameter of type 'ArrayBuffer'. Types of property '[Symbol.toStringTag]' are incompatible. Type '"Uint8Array"' is not assignable to type '"ArrayBuffer"'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. bytes.js(8,32): error TS2345: Argument of type 'Uint8Array<ArrayBuffer>' is not assignable to parameter of type 'ArrayBuffer'. Types of property '[Symbol.toStringTag]' are incompatible. Type '"Uint8Array"' is not assignable to type '"ArrayBuffer"'. checked 21 October 2026
/** @param {ArrayBuffer} buffer */
function byteCount(buffer) {
  return buffer.byteLength;
}

const bytes = new Uint8Array([1, 2, 3]);
export const loose = byteCount(bytes);error TS2345: Argument of type 'Uint8Array<ArrayBuffer>' is not assignable to parameter of type 'ArrayBuffer'.
  Types of property '[Symbol.toStringTag]' are incompatible.
    Type '"Uint8Array"' is not assignable to type '"ArrayBuffer"'.
export const count = byteCount(bytes.buffer);
A Uint8Array is no longer an ArrayBuffer.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": [
    "*.js"
  ]
}

What did not change

No JSDoc tag changed in 5.9. The release notes do not mention JavaScript files, and every example on this site runs on 5.9 as written.

Upgrade notes

From the behavioral changes:

  • Typed arrays passed as ArrayBuffer: use .buffer or a typed-array parameter, and update @types/node.
  • Type argument inference changed in some generic calls; a JSDoc cast or a more specific @param type settles them, since .js files cannot pass type arguments.
  • tsc --init output is different, so scripts that edit it may need changes.

Gotchas

  • new Uint8Array([1, 2, 3]) is a Uint8Array<ArrayBuffer>, so its .buffer is an ArrayBuffer; a typed array of unknown origin is Uint8Array<ArrayBufferLike>, whose .buffer may be a SharedArrayBuffer.

Sources

Last reviewed 20 October 2026.