November 20, 2023·6 min read·engineeringtypescriptmodulesnarrowingcompiler

TypeScript 5.3 models module expectations and runtime predicates

Import attributes communicate with the module host, while improved narrowing follows the predicates JavaScript actually executes. The release also reduces duplicated compiler work and library packaging.

The meaning of a JavaScript operation can depend on behavior outside its familiar syntax. An import delegates interpretation to a host. An instanceof expression can invoke a custom predicate instead of establishing ordinary class membership. TypeScript 5.3 improves how the compiler represents both arrangements, separating facts it can check from decisions made by a runtime or library.

The November 20 release announcement includes import attributes and broader control-flow narrowing. It also documents smaller compiler packaging and optimizations to type comparisons. These changes affect different layers, but each reduces a mismatch between a convenient representation and the operation that representation needs to describe.

I value TypeScript for making framework code navigable and exposing mistakes before execution. That usefulness depends on precision about what a check proves. Accepting syntax is insufficient if it suggests a runtime guarantee that the compiler has not established.

Import attributes communicate an expectation

The new with clause can state the expected interpretation of an imported module. A JSON import can request that the host treat its target as JSON. TypeScript preserves the attributes for the browser, runtime or other module-processing tool rather than assigning universal meanings to their values.

import settings from "./settings.json" with { type: "json" };

The release explicitly says attribute contents are host-specific and not checked by TypeScript. A host can reject an attribute that the compiler accepted. A project therefore needs support in the execution or bundling arrangement that consumes the preserved syntax.

Dynamic import supports the same request through its second argument. ImportCallOptions describes that argument, with the attributes under a with property. Both loading forms can carry expectations without requiring the compiler to implement every possible host interpretation.

The change evolves the earlier import-assertions proposal. Beyond using with instead of assert, it permits attributes to guide resolution and interpretation. The earlier model restricted assertions to checking characteristics after loading. That distinction explains why the update affects more than a preferred keyword.

The release recommends the new syntax for new attribute-bearing imports. Migrating an existing assertion still needs host support for the replacement form. A compiler upgrade supplies its side of the language contract, while the application's module-processing path determines whether the emitted import can run.

Type-only imports also need a resolution choice

TypeScript 5.3 stabilizes resolution-mode on import type. A declaration can ask to resolve a package using import or require semantics. This matters when a package provides different declarations or entry relationships for its module formats.

A type-only import does not execute the corresponding runtime import. It nevertheless has to locate the right declaration. A library describing the require-facing interface can need that choice even inside a file whose other relationships use import semantics.

The attribute is also supported in import types within type expressions. Its resolution guidance stays local to the lookup it qualifies. The release additionally documents support across module-resolution modes, including bundler and node10, instead of restricting the facility to node16 and nodenext.

This gives declaration authors a way to express relationships that were previously awkward or nightly-only. The compiler no longer has to derive every type lookup solely from the surrounding file's default module arrangement. The explicit request can identify which side of a package the declaration intends to describe.

Custom instanceof behavior can prove a structural type

JavaScript allows the right-hand operand of instanceof to provide a Symbol.hasInstance method. That method determines whether the test succeeds. Its result does not necessarily establish that the checked value was constructed by a particular class.

The TypeScript implementation now uses a declared type predicate on that method for narrowing. An object with the required method can participate, and its predicate specifies the type established by success. This follows the customization supported by the JavaScript operation more closely.

The release example tests for a point-like object by checking numeric coordinates. Success permits access to those coordinates. It does not establish the presence of a distance method defined on the class whose custom has-instance method performed the test.

That distinction prevents a misleading inference. A value may satisfy a structural predicate without possessing the rest of a class interface. Narrowing to the declared predicate type gives the caller the facts actually checked by the implementation.

The implementation requires an appropriate type-predicate return declaration. A boolean result alone does not communicate which type it proves. The method's parameter type also constrains the checked argument. Runtime flexibility does not remove the need for a sound declaration of the relationship.

I prefer this treatment to assuming instanceof always means one conventional prototype story. Libraries can customize the runtime operator, so the type system needs a way to describe that customization explicitly. A declaration that follows the predicate is easier to trust than one inferred only from the class name on the right.

Control flow recognizes more conditional forms

Switch statements using true as their discriminant can now narrow based on case conditions. A case testing whether a value is a string can establish that fact in its branch. The compiler also accounts for fallthrough, where a later branch can be reached through more than its own condition.

The result is not equivalent to treating every case as an isolated if statement. Control can continue from a preceding matched case. A later branch may therefore have a union of possible types, and the release example makes that visible.

Explicit boolean comparisons also preserve narrowing. If a type guard is compared with true, TypeScript can retain the guard's information. A programmer's choice to write the comparison no longer hides that relationship from the checker.

These changes broaden the set of existing JavaScript forms the compiler understands. They do not require changing the language's runtime branching behavior. Applications can keep a conditional style while receiving a more accurate analysis of the paths it permits.

Super calls distinguish instance fields from prototype methods

TypeScript 5.3 adds an error when a super property access refers to a class field. An arrow function assigned to a field exists on each instance. A normal method declaration participates in the prototype arrangement through which super performs its lookup.

Calling a base field through super can therefore encounter undefined even when a call through this would find a function. The new check identifies that mismatch before execution. A callable type by itself is not enough to establish that the property exists at the location the operation searches.

This is particularly relevant to code that switches between method declarations and arrow-function fields. Both can provide functions to ordinary callers, but their placement differs. Inheritance code has to preserve that distinction even when the exposed call signatures look similar.

Compiler optimizations reuse available information

The release optimizes some comparisons involving intersections distributed over unions. A type written as an intersection with a union can be normalized into a union of intersections. Comparing every normalized member may be expensive even when the original form already provides a sufficient relationship.

TypeScript retains that original representation for some purposes and can consult it for a faster comparison. If the target appears as an intersection constituent, the compiler may avoid repeating work over a much larger normalized union. The optimization uses information already associated with the type.

JSDoc parsing gains a configurable strategy as well. The compiler can skip comment parsing work it does not need, reducing allocation and related collection costs. Tools consuming the TypeScript API can choose a parsing mode appropriate to their use instead of always storing the same comment representation.

That distinction matters because editor tooling and command-line checking have different information needs. Documentation can be useful for hover information while some consumers only need syntax and type relationships. An API-level choice lets those consumers avoid unnecessary work without removing documentation from the source.

Compiler library consolidation removes duplicate instances

TypeScript previously distributed separate typescript.js and tsserverlibrary.js bundles with substantial overlap. Some server APIs existed only in the latter. Importing both could load duplicate implementations and create problems for code depending on one instance of the compiler API.

In 5.3, typescript.js contains the fuller implementation, and tsserverlibrary.js re-exports it. The announcement reports a package-size reduction of more than twenty percent for the compared packages. Consolidation also addresses the identity problem of accidentally loading two overlapping API implementations.

For tool authors, this is more than a download-size change. Shared compiler objects can be sensitive to which API instance created them. Re-exporting one implementation reduces accidental duplication while preserving the older entry point for consumers that still name it.