Today, Microsoft is pleased to announce the release of TypeScript 5.3, the latest version of the free and open source programming language developed by Microsoft to improve and secure the production of JavaScript code.
If you’re unfamiliar with TypeScript, it’s a language that adds JavaScript type syntax to enable type checking. Type checking helps detect all kinds of problems, such as: B. Typos and forgetting to check for null and undefined. But types go far beyond type checking – the same analyzes of TypeScript’s type checker are used for rich editing tools like autocompletion, code navigation, and refactorings. If you’ve written JavaScript in editors like Visual Studio or VS Code, this experience is actually powered by TypeScript!
To use TypeScript, you can get it via NuGet or via npm with the following command:
npm install -D typescript
What’s new since Beta and RC?
There have been no significant changes since the release of the candidate version.
Since the release of the beta version, an option has been added to prefer type-auto imports only when possible.
The beta version allowed the use of resolution mode for all module resolution settings, but did not document this.
Import attributes
TypeScript 5.3 supports the latest updates to the import attribute proposal.
One use case for import attributes is to provide the runtime with information about the expected format of a module.
1 |
// We only want this to be interpreted as JSON, // not an executable/malicious JavaScript file with a .json extension. import obj from “./something.json” with { type: “json” }; |
The contents of these attributes are not checked by TypeScript because they are host specific, and are simply left unchanged for browsers and runtimes to handle (and potentially make errors).
1 |
// TypeScript agrees with this. // But your browser? Probably not. import * as foo from “./foo.js” with { type: “fluffy bunny” }; |
Dynamic import() calls can also use import attributes via a second argument.
1 |
const obj = waiting import(“./something.json”, { with: { type: “json” } }); |
The expected type of this second argument is defined by a type called ImportCallOptions, which by default simply expects a property called with .
Note that import attributes are an evolution of an earlier proposal called “Import Assertions” implemented in TypeScript 4.5. The most obvious difference is the use of the with keyword instead of the Assert keyword. The less visible difference, however, is that runtimes can now use attributes to control the resolution and interpretation of import paths, while import assertions can only assert certain characteristics after a module is loaded.
Over time, TypeScript will abandon the old import assertion syntax in favor of the proposed import attribute syntax. Existing code that uses assert should be migrated to the with keyword. New code that requires an import attribute should only be used with.
Stable support for resolution mode in import types
In TypeScript 4.7, TypeScript has support for a resolution mode attribute in ///
A corresponding field has also been added only for import assertions for type imports; However, it was only supported in nightly builds of TypeScript. The reasoning was that the import assurances were essentially not intended to guide the resolution of modules. This functionality was therefore deployed experimentally in “Nightly Only” mode to get more feedback.
However, because import attributes can control resolution and useful use cases have been observed, TypeScript 5.3 now supports the resolution-mode attribute for imported types.
1 |
// resolve “pkg” as if we were importing with “require()” import type { TypeFromRequire } from “pkg” with { “resolution-mode”: “require” }; // resolve “pkg” as if we were importing with an “import” import type { TypeFromImport } from “pkg” with { “resolution-mode”: “import” }; MergedType export interface extends TypeFromRequire, TypeFromImport {} |
These import attributes can also be used for import() types.
1 |
export type TypeFromRequire = import(“pkg”, { with: { “resolution-mode”: “require” } }).TypeFromRequire; Export type TypeFromImport = import(“pkg”, { with: { “resolution-mode”: “import” } }).TypeFromImport; MergedType export interface extends TypeFromRequire, TypeFromImport {} |
Resolution mode support in all module modes
Previously, the use of resolution mode was only permitted for the moduleResolution options node16 and nodenext. To make it easier to find specific modules for type purposes, resolve mode now works properly in all other module resolver options such as Bundler and Node10 and simply does not cause an error on Classic.
Shrinkage in a switch (true)
TypeScript 5.3 can now perform condition-based narrowing in any case clause within a switch (true).
1 |
function f(x: unknown) { switch (true) { case typeof x === “string”: // ‘x’ is a ‘string’ here console.log(x.toUpperCase()); // fails… case Array.isArray(x): // ‘x’ is a ‘string | any[]’Here. console.log(x.length); // fails… Default: // ‘x’ is ‘unknown’ here. // … } } |
Limitation when comparing with Boolens
There may be times when you need to make a direct comparison with true or false in a condition. These are generally unnecessary comparisons, but you may prefer them for style reasons or to avoid certain JavaScript correctness issues. Regardless, TypeScript did not recognize these shapes during narrowing.
TypeScript 5.3 now aligns and understands these expressions when delimiting variables.
1 |
interface A { a: string; } interface B { b: string; } Type MyType = A | B; function isA(x: MyType): x is A { return “a” in x; } function someFn(x: MyType) { if (isA(x) === true) { console.log(xa); // functions! } } |
Limit instance of with Symbol.hasInstance
A somewhat esoteric feature of JavaScript is that it is possible to override the behavior of the instanceof operator. To do this, the right value of the instanceof operator must have a specific method called Symbol.hasInstance.
1 |
class Weirdo { static [Symbol.hasInstance](testedValue) { // wait, what? return testedValue === undefined; } } // false console.log(new Thing() exampleof Weirdo); // true console.log(undefined instance of Weirdo); |
To better model this behavior in the instance of, TypeScript now checks whether such a method exists [Symbol.hasInstance] exists and is declared as a type predicate function. If this is the case, the left-tested value of the instance operator is constrained accordingly by this type predicate.
1 |
interface PointLike { x: number; y:number; } class Point implements PointLike { x: number; y:number; constructor(x: number, y: number) { this.x = x; this.y = y; } distanceFromOrigin() { return Math.sqrt(this.x ** 2 + this.y ** 2); } static [Symbol.hasInstance](Value: unknown): Value is PointLike { return !!val && typeof val === “object” && “x” in val && “y” in val && typeof val.x === “number” && typeof val. y === “number”; } } function f(Value: unknown) { if (Value instance of Point) { // Can access both – right! value.x; value.y; // Can’t access it – 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 method [Symbol.hasInstance]. It actually acts as a custom type protection for a PointLike call of a separate type. In the f function it was possible to reduce the evaluated value to a PointLike with “instanceof”, but not to a Point. This means that the x and y properties can be accessed, but not the distanceFromOrigin method.
Verify super property access to instance fields
In JavaScript, it is possible to access a declaration in a base class using the super keyword.
1 |
class Base { someMethod() { console.log(“Base method called!”); } } class Derived extends Base { someMethod() { console.log(“Derived method called!”); super.someMethod(); } } new Derived().someMethod(); // Prints: // Derived method called! //Basic method called! |
This is different from writing something like this.someMethod() as this could result in method overloading. This is a subtle difference, made even more subtle by the fact that the two are often interchangeable if a statement is never overloaded.
1 |
class Base { someMethod() { console.log(“someMethod called!”); } } class Derived extends Base { someOtherMethod() { // These behave identically. this.someMethod(); super.someMethod(); } } new Derived().someOtherMethod(); // Prints: // someMethod called! // someMethod called! |
The problem with their interchangeability is that Super only works for members declared in the prototype – not instance properties. This means that if you write super.someMethod() but someMethod is defined as a field, you will get a runtime error.
1 |
class Base { someMethod = () => { console.log(“someMethod called!”); } } class Derived extends Base { someOtherMethod() { super.someMethod(); } } new Derived().someOtherMethod(); // * // Doesn’t work because super.someMethod is “undefined”. |
TypeScript 5.3 now checks access to property/super method calls more closely to see if they match class fields. If this is the case, a type checking error occurs.
Interactive overlay hints for types
You can now skip type definitions with TypeScript overlay hints! This makes your code easier to navigate.
Settings to favor automatic type imports
Previously, when TypeScript generated automatic imports for something in a type position, it would add a type modifier based on your settings. For example, if you do an automatic import of a person in the following:
The TypeScript editing experience typically added an import for Person as:
1 |
import { Person } from “./types”; export let p: person |
and under certain parameters like verbatimModuleSyntax the type modifier would be added:
1 |
import { type Person } from “./types”; export let p: person |
However, your codebase may not be able to use some of these options, or you may simply prefer explicit type imports whenever possible.
Thanks to a recent change, you can now make this option an editor-specific option in TypeScript. In Visual Studio Code, you can enable it in the UI under TypeScript > Preferences: Prefer Type Only Auto Imports or as a JSON configuration option typescript.preferences.preferTypeOnlyAutoImports.
Optimizations by skipping JSDoc parsing
When running TypeScript over tsc, the compiler now avoids parsing JSDoc. This reduces the parsing time itself, but also reduces the memory usage for storing comments as well as the time spent on garbage collection. Overall, you should see slightly faster builds and faster feedback in –watch mode.
Since not all tools that use TypeScript need to store JSDoc (e.g. typescript-eslint and Prettier), this parsing strategy has been built into the API itself. This allows these tools to benefit from the same memory and speed improvements made to the TypeScript compiler. New comment parsing strategy options are described in JSDocParsingMode.
Optimizations by comparing non-normalized intersections
In TypeScript, unions and intersections always follow a specific shape, although intersections cannot contain union types. This means that if we create an intersection on a union like A & (B | C), that intersection becomes (A & B) | is normalized (A & C). However, in some cases the type system retains its original form for display purposes.
It turns out that the original form can be used for quick and practical comparisons between types.
For example, let’s say we have SomeType & (Type1 | Type2 | … | Type99999NINE) and want to see if it is assignable to SomeType. Remember that we don’t actually have an intersection as a source type – we have a union that looks like this (SomeType & Type1) | (SomeType & Type2) | … … | (SomeType & Type99999NINE). When checking whether a union is assignable to a target type, we need to check whether each member of the union is assignable to the target type, which can be very slow.
TypeScript 5.3 takes a look at the original intersection shape, which has been discarded. When comparing the types, a quick check is performed to determine whether the target is present in one of the components of the source intersection.
Consolidation between tsserverlibrary.js and typescript.js
TypeScript comes with two library files: tsserverlibrary.js and typescript.js. Some APIs are only available in tsserverlibrary.js (like the ProjectService API), which may be useful for some importers. However, the two packages are separate and overlap heavily, duplicating code within the package. Additionally, it may be difficult to use one over the other consistently due to automatic imports or muscle memory. It’s far too easy to accidentally load both modules, and the code may not work correctly on another instance of the API. Even if it works, loading a second bundle increases resource consumption.
Under these conditions, it was decided to combine the two modules. typescript.js now contains what tsserverlibrary.js previously contained, and tsserverlibrary.js simply re-exports typescript.js. Comparing before/after this consolidation, the reduction in package size is as follows:
In other words, this is a reduction in package size of over 20.5%.
Important changes and correction improvements
Changes in lib.d.ts
The types generated for the DOM can impact your code base.
Check super access to instance properties
TypeScript 5.3 now detects when the declaration references a super-access property. is a class field and throws an error. This helps avoid errors that might occur during execution.
What are the next steps?
Although the iteration plan is not yet public, Microsoft is already working on TypeScript 5.4. TypeScript 5.4 is expected to have a stable release by the end of February 2024.
Source : Microsoft
And you ?
What do you think about this version 5.3 of TypeScript and the features it includes?
What features would you like to see in the next version of TypeScript?
See also
Microsoft announces the availability of TypeScript 5.3 release candidate, documents the resolution mode and adds an automatic type import preference option
Microsoft announces the availability of TypeScript 5.3 Beta and provides a short list of what’s new in TypeScript 5.3 Beta
Originally posted 2023-11-21 13:15:03.