August 1, 2025·6 min read·engineeringtypescriptconfigurationmodulestype-safety

TypeScript 5.9 makes a new project's checking policy more explicit

TypeScript 5.9 replaces the generated configuration template with a smaller set of deliberate settings. Indexed access, optional properties, module behavior, and ambient types become explicit decisions alongside new deferred imports.

A configuration generator teaches users what a project considers normal. A long file containing commented alternatives makes options discoverable, but it can leave the actual checking policy difficult to identify. TypeScript 5.9 changes that starting point, generating a smaller configuration with more deliberate choices enabled.

The August 1 release replaces the previous tsc --init template. The announcement says users commonly deleted much of the old file and used editor completion or reference documentation to discover options. Its new template retains comments for selected choices while making several source and checking assumptions explicit.

This is a change to generated configuration. It does not rewrite existing projects or make every enabled template setting an unconditional compiler default. That difference matters when two projects using the same compiler version report different errors.

I am ambivalent about TypeScript, but its ecosystem can produce excellent web applications when the framework and language conventions are followed carefully. A generated configuration should help make those conventions visible. Otherwise, a project can inherit important behavior without knowing which setting established it.

Indexed access should account for absence

The new template enables noUncheckedIndexedAccess. This option already existed before 5.9. Its promotion into the generated starting configuration expresses a preference about how new projects should handle potentially absent indexed values.

The TypeScript 4.1 explanation describes the problem with optimistic index signatures. A dictionary type can describe the type of values associated with arbitrary keys without establishing that every possible key exists.

With unchecked indexed access disabled through the option, the compiler includes possible absence in the result. Code then has to handle that case before treating a lookup as a definite value.

const counts: Record<string, number> = {}
const count = counts["pending"]

if (count !== undefined) {
  console.log(count.toFixed(0))
}

The declaration says stored values are numbers. It does not populate the dictionary. The conditional supplies the runtime test needed to narrow the lookup result.

This is a useful distinction for interfaces driven by collections. A type describing a set of possible entries does not guarantee that data has arrived for a particular key. The checking policy helps keep that absence visible while the application decides how to present it.

Missing properties differ from undefined values

The template also enables exactOptionalPropertyTypes. The TypeScript 4.4 release notes explain why an absent property is not identical to a present property containing undefined.

JavaScript operations that enumerate or copy properties can observe that difference. A declaration saying a property may be absent should not automatically say the property may be present with an undefined value.

The following interface expresses optional presence with a numeric value when present. The second assignment is rejected when exact optional property checking is enabled.

interface Options {
  retries?: number
}

const omitted: Options = {}
const presentButUndefined: Options = { retries: undefined }

If explicitly undefined is meaningful, the type can say so through number | undefined. That makes the accepted state a source decision instead of an implicit widening of every optional property.

The option predates this release and is separate from the strict family. Enabling both in the template matters because strict checking alone does not express every policy the new-project configuration chooses.

The two absence-related options affect different directions. Indexed checking concerns reading a value that may not exist. Exact optional properties constrain the states permitted when an object is constructed or assigned. Together they make more of the data model's absence semantics explicit.

Ambient types no longer arrive without an explicit list

The generated file includes types: []. The announcement explains that projects often load more declaration packages from node_modules/@types than they need. An empty list limits automatic inclusion of those packages.

The template comments show how a Node project can opt into Node types. That is an environment decision. A browser application should not gain assumptions about server-side globals merely because a dependency brought a declaration package into its installation.

This does not mean declarations imported by application modules become irrelevant. The concern is automatic ambient inclusion, where types can affect a project's global environment without an explicit source import. Keeping that environment deliberate makes the checker a more accurate model of execution.

The template's moduleDetection: "force" similarly expresses the expectation that implementation files are modules. That reduces reliance on whether a particular file happens to contain a syntactic marker that makes it a module.

These choices make the initial configuration more prescriptive despite its smaller size. Removing comments is only part of the change. The generated settings state assumptions about scope, environment, and missing data that developers previously often added themselves.

A stable Node mode differs from a moving reference

TypeScript 5.9 adds module: "node20". It models the Node 20 generation, including supported interoperability between CommonJS and eligible ES modules. It also rejects older import assertions in favor of import attributes.

Unlike nodenext, the stable mode is not intended to accumulate new Node behavior over time. It implies an ES2023 target unless overridden, while nodenext implies the moving ESNext target.

This makes the setting a compatibility commitment. A project using a particular runtime generation can describe it without following every later module-model update. Another project may intentionally choose the moving model because it controls deployment accordingly.

The generated template uses nodenext. That choice still needs evaluation against the project. A browser application whose bundler owns module resolution has different requirements from a directly executed Node program.

My preference is for this difference to remain visible in configuration. TypeScript's value comes partly from modeling an execution environment before code runs. A configuration that models the wrong environment can provide consistent answers about a program the runtime will never execute in that form.

Deferred imports preserve a runtime requirement

The release also supports import defer, which postpones synchronous module evaluation until access through an imported namespace. The module graph still loads in advance. This addresses startup computation separately from retrieving source.

The ECMAScript proposal explains why dynamic import is a different mechanism. Dynamic import introduces an asynchronous result, while deferred evaluation aims to preserve a synchronous consuming API after loading has completed.

TypeScript preserves the new syntax without downlevel transformation. A supporting runtime or bundler must implement it, and the compiler only permits it under preserve or esnext module modes. The generated nodenext template therefore does not imply that every new syntax feature works under its unchanged settings.

That apparent tension is useful to understand. A configuration template supplies a starting policy, not a promise that one combination fits all use cases. Supporting new JavaScript syntax and selecting a conservative runtime model are separate compiler responsibilities.

Editor and checker improvements affect existing projects

Existing projects can benefit without regenerating their configuration. The release adds summary descriptions to many DOM declarations and previews expandable editor hovers. These make types and APIs easier to inspect where the code is being edited.

The hover changes distinguish the size of an explanation from its depth. A larger maximum length lets the editor display more of an existing type description. Expandable hovers let a developer request a deeper view of a type alias, then return to the earlier view. Those operations address different reasons a tooltip can omit useful information.

The announcement keeps expandable hovers in preview and asks for editor feedback. Stable release of the compiler does not automatically make every associated editor feature final. That matters when comparing the behavior of the command-line checker with the interface through which a developer examines its results.

The checker also caches intermediate generic instantiations when an instantiation is already underway. The announcement identifies complex libraries such as Zod and tRPC as cases where repeated work can affect performance and instantiation-depth diagnostics. This is implementation work beneath an unchanged type-level API.

There are also compatibility changes. Updated buffer declarations can produce new errors involving ArrayBufferLike, typed arrays, and Node Buffer. The announcement recommends checking compatible Node declarations and expressing the appropriate underlying buffer type when needed.

Inference fixes can introduce different inferred results or errors too. That is separate from a project enabling stricter settings through a new template. Understanding whether a diagnostic came from configuration, declarations, or inference makes the change easier to assess.

TypeScript 5.9 therefore gives new projects a more explicit initial policy while continuing to evolve the checker and editor used by existing projects. The useful result is a clearer description of missing values, ambient assumptions, and module behavior. Each remains a choice the project can inspect and align with its own runtime.