TypeScript 5.9 Study Guide (for JavaScript, Node.js, and React developers)

From Qiki
Jump to navigation Jump to search

TypeScript 5.9 Study Guide (for JavaScript, Node.js, and React developers)

Last verified: 2026-01-13 Latest stable release line shown on the TypeScript site: TypeScript 5.9 (TypeScript) Latest patch listed on GitHub releases: 5.9.3 (GitHub) TypeScript 5.9 announcement date: 2025-08-01 (Microsoft for Developers)

This document focuses on what’s new/important in TypeScript 5.9, plus the practical configuration + patterns you’ll use in Node.js and React.


Table of contents



1. What “latest TypeScript” means

TypeScript has:

  • A stable release line (today that’s 5.9 on the official site). (TypeScript)
  • Patch releases within that line (GitHub currently lists 5.9.3 as the latest patch). (GitHub)

Also: the TypeScript team has been working on a new native toolchain (“TypeScript 7” line) and describes TypeScript 6.0 as a “bridge” release, but that’s roadmap context—not what most projects call “latest stable TypeScript” today. (Microsoft for Developers)



2. Install and verify TypeScript 5.9

Install per-project (recommended)

npm install --save-dev typescript
npx tsc -v

This is the standard workflow for Node.js projects, and using a lockfile keeps everyone on the same TS version. (TypeScript)

Global install (okay for quick experiments)

npm install -g typescript
tsc -v

The TypeScript download page explicitly calls out global installs as convenient for one-offs but recommends per-project installs for reproducibility. (TypeScript)



3. The biggest change in 5.9: a new default tsc --init

In TypeScript 5.9, running tsc --init now generates a much smaller, more prescriptive tsconfig.json by default, instead of a huge commented file. (TypeScript)

Key defaults from that generated config include (summarized):

  • module: "nodenext"
  • target: "esnext"
  • types: [] (important!)
  • sourceMap: true
  • declaration: true
  • declarationMap: true
  • noUncheckedIndexedAccess: true
  • exactOptionalPropertyTypes: true
  • strict: true
  • jsx: "react-jsx"
  • verbatimModuleSyntax: true
  • isolatedModules: true
  • noUncheckedSideEffectImports: true
  • moduleDetection: "force"
  • skipLibCheck: true (TypeScript)

Why this matters to you (Node/React dev)

This new default pushes you toward:

  • Modern JS output (high target) (TypeScript)
  • Modern module semantics (nodenext + explicit module detection) (TypeScript)
  • Cleaner module emit rules (verbatimModuleSyntax) (TypeScript)
  • Bundler-friendly correctness (isolatedModules) (TypeScript)
  • Stricter correctness checks (like noUncheckedIndexedAccess) (TypeScript)

But: the new default also includes settings you may want to change depending on whether you’re building an app or a library—especially declaration/declarationMap and types: []. (TypeScript)



4. What’s new in TypeScript 5.9

4.1 import defer

TypeScript 5.9 adds support for the ECMAScript deferred module evaluation proposal via:

import defer * as feature from "./some-feature.js";

Important constraints and behavior:

  • Only namespace imports are allowed (import defer * as …). (TypeScript)
  • The module is loaded, but not evaluated until you access an export (e.g., feature.someExport). (TypeScript)
  • TypeScript does not downlevel/transform import defer. It’s intended for runtimes/tools that support it, and it only works under --module preserve and --module esnext. (TypeScript)

When it’s useful: deferring expensive initialization or side effects until a feature is actually used (think: optional subsystems, “only on client” code, feature flags, etc.). (TypeScript)

Compare with import() (dynamic import):

  • import() is asynchronous and typically loads+evaluates the module immediately when awaited.
  • import defer is designed to delay evaluation until first use of an export (even though the module is already loaded). (TypeScript)



4.2 Stable Node 20 mode: --module node20 / --moduleResolution node20

TypeScript 5.9 introduces stable Node 20 modes:

  • --module node20
  • --moduleResolution node20

The release notes describe node20 as:

  • Intended to model Node.js v20
  • Unlikely to pick up new behaviors over time (unlike nodenext) (TypeScript)
  • --module node20 also implies --target es2023 unless you override it (whereas nodenext implies the floating esnext). (TypeScript)

The module reference also notes node20 includes support for require() of ESM in the appropriate interop cases. (TypeScript)

Practical guidance:

  • If your production runtime is fixed at Node 20, node20 is a good “stable anchor.”
  • If you want TypeScript to track the newest Node behaviors more aggressively, use nodenext (and accept occasional behavior shifts). (TypeScript)



4.3 Better DOM API tooltips

TypeScript 5.9 includes summary descriptions for many DOM APIs (based on MDN documentation), improving the in-editor learning experience when you hover DOM types. (TypeScript)



4.4 Better editor hovers

TypeScript 5.9 previews expandable hovers (a “verbosity” control with + / -) in editors like VS Code. (TypeScript)

It also supports a configurable maximum hover length via the VS Code setting js/ts.hover.maximumLength, and increases the default hover length. (TypeScript)



4.5 Performance improvements

Two notable optimizations called out in the 5.9 notes:

  • Caching intermediate instantiations during complex type substitution (helps performance and can reduce “excessive type instantiation depth” problems in heavily-generic libraries). (TypeScript)
  • Faster file/directory existence checks (less allocation overhead in a hot path). (TypeScript)



4.6 Breaking-ish changes to watch for

lib.d.ts changes: ArrayBuffer vs TypedArrays (including Node Buffer)

TypeScript 5.9 changes built-in types so that ArrayBuffer is no longer a supertype of several TypedArray types, including Node’s Buffer (which is a subtype of Uint8Array). This can surface new errors around BufferSource, ArrayBufferLike, etc. (TypeScript)

Mitigations suggested in the release notes include:

  • Update to the latest @types/node first.
  • Use a more specific underlying buffer type (e.g., Uint8Array<ArrayBuffer> instead of Uint8Array defaulting to ArrayBufferLike).
  • Pass typedArray.buffer when an API expects an ArrayBuffer. (TypeScript)

Type argument inference changes

TypeScript 5.9 includes changes meant to fix “leaks” of type variables during inference. This may produce new errors in some codebases, and often the fix is to add explicit type arguments to generic calls. (TypeScript)



5. Modern TS config, explained (Node + React)

This section explains the settings you’re most likely to see (especially if you start from tsc --init in 5.9).

types: why types: [] can surprise you

  • By default, TypeScript includes all “visible” @types/* packages.
  • If you specify types, TypeScript only includes what you list.
  • Therefore types: [] means include no global @types packages. (TypeScript)

Common consequences:

  • Node projects need "types": ["node"] plus @types/node. (TypeScript)
  • React projects may need "types": ["react", "react-dom"] (or you can remove the types setting and let TS auto-include visible @types packages). (TypeScript)

verbatimModuleSyntax: predictable import/export output

With verbatimModuleSyntax, TypeScript keeps imports/exports without a type modifier, and drops anything explicitly marked type. This simplifies “import elision” and makes output closer to “what you wrote.” (TypeScript)

This is especially valuable when you want consistency across:

  • tsc emit
  • Babel/swc/esbuild transforms
  • mixed ESM/CJS packages (TypeScript)

isolatedModules: bundler/transpiler compatibility

isolatedModules warns you about TS patterns that can’t be safely transpiled one file at a time (common with Babel/swc-style transforms). It doesn’t change runtime behavior; it’s a correctness guardrail. (TypeScript)

noUncheckedSideEffectImports: catch typos, but watch asset imports

By default, TypeScript may silently ignore unresolved side-effect imports (import "x";). With noUncheckedSideEffectImports, TypeScript errors if it can’t resolve them. (TypeScript)

If you import assets like CSS, you’ll often solve this by adding an ambient module declaration:

// src/globals.d.ts
declare module "*.css" {}

That exact workaround is recommended in the option docs. (TypeScript)

moduleDetection: "force": treat every file as a module

moduleDetection controls how TS decides whether a file is a “script” or a “module.” The "force" option treats every non-.d.ts file as a module, which avoids a lot of accidental-global-script weirdness. (TypeScript)

noUncheckedIndexedAccess and exactOptionalPropertyTypes: stricter correctness

  • noUncheckedIndexedAccess adds undefined when you access properties via “maybe present” keys (like index signatures). (TypeScript)
  • exactOptionalPropertyTypes treats prop?: T as “either missing, or T” (not “T | undefined”), which catches subtle bugs around explicitly assigning undefined. (TypeScript)

skipLibCheck: faster builds, lower strictness for .d.ts

skipLibCheck skips type-checking of declaration files. It can speed builds and reduce friction during upgrades, at the cost of some type-system accuracy. (TypeScript)



These are starting points; adjust for your runtime and toolchain.

6.1 Node.js (Node 20, ESM)

Use stable Node 20 semantics.

{
  "compilerOptions": {
    "target": "es2023",
    "module": "node20",
    "moduleResolution": "node20",

    "rootDir": "src",
    "outDir": "dist",

    "strict": true,

    // Important if you started from TS 5.9's `tsc --init` default:
    "types": ["node"],

    "sourceMap": true,

    // For an app you might not need these:
    "declaration": false,
    "declarationMap": false,

    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "moduleDetection": "force",

    "skipLibCheck": true
  },
  "include": ["src"]
}

Why these choices:

  • node20 is a stable mode intended to model Node 20 behavior. (TypeScript)
  • types: ["node"] is needed if you otherwise have types: [] (the 5.9 tsc --init default). (TypeScript)
  • verbatimModuleSyntax and isolatedModules are common modern defaults and are part of the new 5.9 init template. (TypeScript)

If you do want .d.ts output (e.g., you’re building a library), flip declaration back to true. (TypeScript)


6.2 React (bundler: Vite/Webpack/Next, etc.)

For most React apps, you often want TS to type-check only and let your bundler transpile.

{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",

    // Bundler-friendly resolution rules
    "moduleResolution": "bundler",

    "jsx": "react-jsx",

    "strict": true,

    // Let the bundler emit JS
    "noEmit": true,

    // If you started from TS 5.9's init, don't forget types
    // (or remove "types" entirely to use default visibility).
    "types": ["react", "react-dom"],

    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "moduleDetection": "force",

    "skipLibCheck": true
  },
  "include": ["src"]
}

Notes:

  • moduleResolution: "bundler" is documented as a mode for bundler-style projects. (TypeScript)
  • jsx: "react-jsx" is the 5.9 init default and is explained in the JSX compiler option docs. (TypeScript)
  • noEmit makes TypeScript skip output, which is ideal if Babel/swc/esbuild handles transpilation. (TypeScript)
  • If you keep types, remember it changes which @types packages are loaded globally. (TypeScript)



6.3 Libraries (publish types)

If you publish a package, you typically want .d.ts output:

{
  "compilerOptions": {
    "target": "es2020",
    "module": "esnext",
    "moduleResolution": "bundler",

    "rootDir": "src",
    "outDir": "dist",

    "declaration": true,
    "declarationMap": true,

    "strict": true,
    "verbatimModuleSyntax": true,
    "isolatedModules": true,

    "skipLibCheck": true
  },
  "include": ["src"]
}
  • declaration generates .d.ts describing your public API. (TypeScript)
  • declarationMap helps editors jump from .d.ts back to original .ts. (TypeScript)



7. React + TypeScript patterns you’ll use constantly

Strong prop typing without ceremony

type ButtonProps =
  | { variant: "primary"; onClick: () => void }
  | { variant: "link"; href: string };

export function Button(props: ButtonProps) {
  if (props.variant === "link") {
    return <a href={props.href}>Link</a>;
  }
  return <button onClick={props.onClick}>Primary</button>;
}

Why this rocks:

  • Discriminated unions give you automatic narrowing based on variant.
  • You reduce runtime checks and avoid invalid prop combos.

Typed event handlers

function SearchBox() {
  const onChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
    console.log(e.currentTarget.value);
  };
  return <input onChange={onChange} />;
}

useState with correct inference

const [count, setCount] = useState(0);      // number inferred
const [user, setUser] = useState<User|null>(null);

Use noUncheckedSideEffectImports safely with CSS/assets

If you enable noUncheckedSideEffectImports, set up ambient modules for assets (CSS, SVG, etc.) so import "./styles.css" stays type-safe. (TypeScript)



8. TypeScript type system refresher (high leverage)

If you already write JS/React daily, these are the TS concepts that pay off fastest:

  1. Union types and narrowing Use unions to model “states” and branches, and let TS narrow with if, switch, in, typeof, etc.

  2. unknown over any unknown forces you to validate before use—ideal for API responses.

  3. Generics (with constraints) Build reusable helpers:

    function first<T>(arr: readonly T[]): T | undefined {
      return arr[0];
    }
    
  4. Mapped + conditional types These power a lot of modern library types (and your own schema/type helpers).

  5. satisfies + as const for “validated literals” Great for config objects where you want literal types but still want validation.



9. Upgrade checklist to 5.9

  1. Upgrade TypeScript (project devDependency) and run a full type-check. (TypeScript)
  2. Watch for ArrayBuffer/TypedArray/Buffer errors caused by lib.d.ts changes. (TypeScript)
    • First try updating @types/node
    • Then consider using typedArray.buffer or more specific typed array parameters (as described in the release notes) (TypeScript)
  3. If you see new generic inference failures, add explicit type arguments where needed. (TypeScript)
  4. If you run tsc --init and copy that config into a React/Node project, double-check:
    • types (because types: [] is restrictive) (TypeScript)
    • whether you really want declaration / declarationMap in an app (TypeScript)



10. What’s coming next (6.0 and the native TypeScript 7 line)

The TypeScript team’s roadmap update (Dec 2025) states:

  • TypeScript 6.0 will be the last release based on the existing TypeScript/JavaScript codebase (no 6.1 planned, though patch releases may happen). (Microsoft for Developers)
  • 6.0 is described as a bridge between the 5.9 line and 7.0. (Microsoft for Developers)
  • They discuss running the existing typescript package side-by-side with a native preview package and using tsgo for faster type-checking in some workflows. (Microsoft for Developers)

This is mostly “keep an eye on it” information unless you’re chasing very large-project performance improvements.



11. Practice exercises

  1. Recreate the 5.9 tsc --init defaults, then modify them for:
    • Node 20 backend
    • React bundler frontend (TypeScript)
  2. Try import defer:
    • Create a module with obvious side effects (console.log, expensive initialization).
    • Import it with import defer and confirm evaluation timing. (TypeScript)
  3. Fix a Buffer/ArrayBuffer typing issue:
    • Write a small function that accepts ArrayBuffer.
    • Pass a Uint8Array / Buffer, and fix it via .buffer or more specific typing. (TypeScript)
  4. Turn on noUncheckedSideEffectImports and keep your CSS imports working:
    • Add declare module "*.css" {} in a global .d.ts. (TypeScript)



12. References