TypeScript 5.3

Import Attributes

TypeScript 5.3 supports the latest updates to the import attributes proposal.

One use-case of import attributes is to provide information about the expected format of a module to the runtime.

ts
// We only want this to be interpreted as JSON,
// not a runnable/malicious JavaScript file with a `.json` extension.
importobjfrom"./something.json"with { type: "json" };

The contents of these attributes are not checked by TypeScript since they’re host-specific, and are simply left alone so that browsers and runtimes can handle them (and possibly error).

ts
// TypeScript is fine with this.
// But your browser? Probably not.
import*asfoofrom"./foo.js"with { type: "fluffybunny" };

Dynamic import() calls can also use import attributes through a second argument.

ts
constobj = awaitimport("./something.json", {
with: { type:"json" }
});

The expected type of that second argument is defined by a type called ImportCallOptions, which by default just expects a property called with.

Note that import attributes are an evolution of an earlier proposal called “import assertions”, which were implemented in TypeScript 4.5. The most obvious difference is the use of the with keyword over the assert keyword. But the less-visible difference is that runtimes are now free to use attributes to guide the resolution and interpretation of import paths, whereas import assertions could only assert some characteristics after loading a module.

Over time, TypeScript will be deprecating the old syntax for import assertions in favor of the proposed syntax for import attributes. Existing code using assert should migrate towards the with keyword. New code that needs an import attribute should use with exclusively.

We’d like to thank Oleksandr Tarasiuk for implementing this proposal! And we’d also like to call out Wenlu Wang for their implementation of import assertions!

Stable Support resolution-mode in Import Types

In TypeScript 4.7, TypeScript added support for a resolution-mode attribute in /// <reference types="..." /> to control whether a specifier should be resolved via import or require semantics.

ts
/// <reference types="pkg" resolution-mode="require" />
// or
/// <reference types="pkg" resolution-mode="import" />

A corresponding field was added to import assertions on type-only imports as well; however, it was only supported in nightly versions of TypeScript. The rationale was that in spirit, import assertions were not intended to guide module resolution. So this feature was shipped experimentally in a nightly-only mode to get more feedback.

But given that import attributes can guide resolution, and that we’ve seen reasonable use-cases, TypeScript 5.3 now supports the resolution-mode attribute for import type.

ts
// Resolve `pkg` as if we were importing with a `require()`
importtype { TypeFromRequire } from"pkg"with {
"resolution-mode": "require"
};
// Resolve `pkg` as if we were importing with an `import`
importtype { TypeFromImport } from"pkg"with {
"resolution-mode": "import"
};
exportinterfaceMergedTypeextendsTypeFromRequire, TypeFromImport {}

These import attributes can also be used on import() types.

ts
exporttypeTypeFromRequire =
import("pkg", { with: { "resolution-mode":"require" } }).TypeFromRequire;
exporttypeTypeFromImport =
import("pkg", { with: { "resolution-mode":"import" } }).TypeFromImport;
exportinterfaceMergedTypeextendsTypeFromRequire, TypeFromImport {}

For more information, check out the change here

resolution-mode Supported in All Module Modes

Previously, using resolution-mode was only allowed under the moduleResolution options node16 and nodenext. To make it easier to look up modules specifically for type purposes, resolution-mode now works appropriately in all other moduleResolution options like bundler, node10, and simply doesn’t error under classic.

For more information, see the implementing pull request.

switch (true) Narrowing

TypeScript 5.3 now can perform narrowing based on conditions in each case clause within a switch (true).

ts
functionf(x: unknown) {
switch (true) {
casetypeofx === "string":
// 'x' is a 'string' here
console.log(x.toUpperCase());
// falls through...
caseArray.isArray(x):
// 'x' is a 'string | any[]' here.
console.log(x.length);
// falls through...
default:
// 'x' is 'unknown' here.
// ...
}
}

This feature was spearheaded initial work by Mateusz Burzyński We’d like to extend a “thank you!” for this contribution.

Narrowing On Comparisons to Booleans

Occasionally you may find yourself performing a direct comparison with true or false in a condition. Usually these are unnecessary comparisons, but you might prefer it as a point of style, or to avoid certain issues around JavaScript truthiness. Regardless, previously TypeScript just didn’t recognize such forms when performing narrowing.

TypeScript 5.3 now keeps up and understands these expressions when narrowing variables.

ts
interfaceA {
a: string;
}
interfaceB {
b: string;
}
typeMyType = A | B;
functionisA(x: MyType): xisA {
return"a"inx;
}
functionsomeFn(x: MyType) {
if (isA(x) === true) {
console.log(x.a); // works!
}
}

We’d like to thank Mateusz Burzyński for the pull request that implemented this.

instanceof Narrowing Through Symbol.hasInstance

A slightly esoteric feature of JavaScript is that it is possible to override the behavior of the instanceof operator. To do so, the value on the right side of the instanceof operator needs to have a specific method named by Symbol.hasInstance.

js
classWeirdo {
static [Symbol.hasInstance](testedValue) {
// wait, what?
returntestedValue === undefined;
}
}
// false
console.log(newThing() instanceofWeirdo);
// true
console.log(undefinedinstanceofWeirdo);

To better model this behavior in instanceof, TypeScript now checks if such a [Symbol.hasInstance] method exists and is declared as a type predicate function. If it does, the tested value on the left side of the instanceof operator will be narrowed appropriately by that type predicate.

ts
interfacePointLike {
x: number;
y: number;
}
classPointimplementsPointLike {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
distanceFromOrigin() {
returnMath.sqrt(this.x ** 2 + this.y ** 2);
}
static [Symbol.hasInstance](val: unknown): valisPointLike {
return !!val && typeofval === "object" &&
"x"inval && "y"inval &&
typeofval.x === "number" &&
typeofval.y === "number";
}
}
functionf(value: unknown) {
if (valueinstanceofPoint) {
// Can access both of these - correct!
value.x;
value.y;
// Can't access this - we have a 'PointLike',
// but we don't *actually* have a 'Point'.
value.distanceFromOrigin();
}
}

As you can see in this example, Point defines its own [Symbol.hasInstance] method. It actually acts as a custom type guard over a separate type called PointLike. In the function f, we were able to narrow value down to a PointLike with instanceof, but not a Point. That means that we can access the properties x and y, but not the method distanceFromOrigin.

For more information, you can read up on this change here.

Checks for super Property Accesses on Instance Fields

In JavaScript, it’s possible to access a declaration in a base class through the super keyword.

js
classBase {
someMethod() {
console.log("Base method called!");
}
}
classDerivedextendsBase {
someMethod() {
console.log("Derived method called!");
super.someMethod();
}
}
newDerived().someMethod();
// Prints:
// Derived method called!
// Base method called!

This is different from writing something like this.someMethod(), since that could invoke an overridden method. This is a subtle distinction, made more subtle by the fact that often the two can be interchangeable if a declaration is never overridden at all.

js
classBase {
someMethod() {
console.log("someMethod called!");
}
}
classDerivedextendsBase {
someOtherMethod() {
// These act identically.
this.someMethod();
super.someMethod();
}
}
newDerived().someOtherMethod();
// Prints:
// someMethod called!
// someMethod called!

The problem is using them interchangeably is that super only works on members declared on the prototype — not instance properties. That means that if you wrote super.someMethod(), but someMethod was defined as a field, you’d get a runtime error!

ts
classBase {
someMethod = () => {
console.log("someMethod called!");
}
}
classDerivedextendsBase {
someOtherMethod() {
super.someMethod();
}
}
newDerived().someOtherMethod();
// 💥
// Doesn't work because 'super.someMethod' is 'undefined'.

TypeScript 5.3 now more-closely inspects super property accesses/method calls to see if they correspond to class fields. If they do, we’ll now get a type-checking error.

This check was contributed thanks to Jack Works!

Interactive Inlay Hints for Types

TypeScript’s inlay hints now support jumping to the definition of types! This makes it easier to casually navigate your code.

Ctrl-clicking an inlay hint to jump to the definition of a parameter type.

See more at the implementation here.

Settings to Prefer type Auto-Imports

Previously when TypeScript generated auto-imports for something in a type position, it would add a type modifier based on your settings. For example, when getting an auto-import on Person in the following:

ts
exportletp: Person

TypeScript’s editing experience would usually add an import for Person as:

ts
import { Person } from"./types";
exportletp: Person

and under certain settings like verbatimModuleSyntax, it would add the type modifier:

ts
import { typePerson } from"./types";
exportletp: Person

However, maybe your codebase isn’t able to use some of these options; or you just have a preference for explicit type imports when possible.

With a recent change, TypeScript now enables this to be an editor-specific option. In Visual Studio Code, you can enable it in the UI under “TypeScript › Preferences: Prefer Type Only Auto Imports”, or as the JSON configuration option typescript.preferences.preferTypeOnlyAutoImports

Optimizations by Skipping JSDoc Parsing

When running TypeScript via tsc, the compiler will now avoid parsing JSDoc. This drops parsing time on its own, but also reduces memory usage to store comments along with time spent in garbage collection. All-in-all, you should see slightly faster compiles and quicker feedback in --watch mode.

The specific changes can be viewed here.

Because not every tool using TypeScript will need to store JSDoc (e.g. typescript-eslint and Prettier), this parsing strategy has been surfaced as part of the API itself. This can enable these tools to gain the same memory and speed improvements we’ve brought to the TypeScript compiler. The new options for comment parsing strategy are described in JSDocParsingMode. More information is available on this pull request.

Optimizations by Comparing Non-Normalized Intersections

In TypeScript, unions and intersections always follow a specific form, where intersections can’t contain union types. That means that when we create an intersection over a union like A & (B | C), that intersection will be normalized into (A & B) | (A & C). Still, in some cases the type system will maintain the original form for display purposes.

It turns out that the original form can be used for some clever fast-path comparisons between types.

For example, let’s say we have SomeType & (Type1 | Type2 | ... | Type99999NINE) and we want to see if that’s assignable to SomeType. Recall that we don’t really have an intersection as our source type — we have a union that looks like (SomeType & Type1) | (SomeType & Type2) | ... |(SomeType & Type99999NINE). When checking if a union is assignable to some target type, we have to check if every member of the union is assignable to the target type, and that can be very slow.

In TypeScript 5.3, we peek at the original intersection form that we were able to tuck away. When we compare the types, we do a quick check to see if the target exists in any constituent of the source intersection.

For more information, see this pull request.

Consolidation Between tsserverlibrary.js and typescript.js

TypeScript itself ships two library files: tsserverlibrary.js and typescript.js. There are certain APIs available only in tsserverlibrary.js (like the ProjectService API), which may be useful to some importers. Still, the two are distinct bundles with a lot of overlap, duplicating code in the package. What’s more, it can be challenging to consistently use one over the other due to auto-imports or muscle memory. Accidentally loading both modules is far too easy, and code may not work properly on a different instance of the API. Even if it does work, loading a second bundle increases resource usage.

Given this, we’ve decided to consolidate the two. typescript.js now contains what tsserverlibrary.js used to contain, and tsserverlibrary.js now simply re-exports typescript.js. Comparing the before/after of this consolidation, we saw the following reduction in package size:

BeforeAfterDiffDiff (percent)
Packed6.90 MiB5.48 MiB-1.42 MiB-20.61%
Unpacked38.74 MiB30.41 MiB-8.33 MiB-21.50%
BeforeAfterDiffDiff (percent)
lib/tsserverlibrary.d.ts570.95 KiB865.00 B-570.10 KiB-99.85%
lib/tsserverlibrary.js8.57 MiB1012.00 B-8.57 MiB-99.99%
lib/typescript.d.ts396.27 KiB570.95 KiB+174.68 KiB+44.08%
lib/typescript.js7.95 MiB8.57 MiB+637.53 KiB+7.84%

In other words, this is over a 20.5% reduction in package size.

For more information, you can see the work involved here.

Breaking Changes and Correctness Improvements

lib.d.ts Changes

Types generated for the DOM may have an impact on your codebase. For more information, see the DOM updates for TypeScript 5.3.

Checks for super Accesses on Instance Properties

TypeScript 5.3 now detects when the declaration referenced by a super. property access is a class field and issues an error. This prevents errors that might occur at runtime.

See more on this change here.

The TypeScript docs are an open source project. Help us improve these pages by sending a Pull Request

Contributors to this page:
ABAndrew Branch (6)
ELEliran Levi (1)

Last updated: Apr 28, 2025