Was this page helpful?

TypeScript 4.7

ECMAScript Module Support in Node.js

For the last few years, Node.js has been working to support ECMAScript modules (ESM).This has been a very difficult feature, since the Node.js ecosystem is built on a different module system called CommonJS (CJS).Interoperating between the two brings large challenges, with many new features to juggle;however, support for ESM in Node.js was largely implemented in Node.js 12 and later.Around TypeScript 4.5 we rolled out nightly-only support for ESM in Node.js to get some feedback from users and let library authors ready themselves for broader support.

TypeScript 4.7 adds this functionality with two newmodule settings:node16 andnodenext.

jsonc
{
"compilerOptions": {
"module":"node16",
}
}

These new modes bring a few high-level features which we’ll explore here.

type inpackage.json and New Extensions

Node.js supportsa new setting inpackage.json calledtype."type" can be set to either"module" or"commonjs".

jsonc
{
"name":"my-package",
"type":"module",
"//":"...",
"dependencies": {
}
}

This setting controls whether.js and.d.ts files are interpreted as ES modules or CommonJS modules, and defaults to CommonJS when not set.When a file is considered an ES module, a few different rules come into play compared to CommonJS:

  • import/export statements can be used.
  • Top-levelawait can be used
  • Relative import paths need full extensions (we have to writeimport "./foo.js" instead ofimport "./foo").
  • Imports might resolve differently from dependencies innode_modules.
  • Certain global-like values likerequire andmodule cannot be used directly.
  • CommonJS modules get imported under certain special rules.

We’ll come back to some of these.

To overlay the way TypeScript works in this system,.ts and.tsx files now work the same way.When TypeScript finds a.ts,.tsx,.js, or.jsx file, it will walk up looking for apackage.json to see whether that file is an ES module, and use that to determine:

  • how to find other modules which that file imports
  • and how to transform that file if producing outputs

When a.ts file is compiled as an ES module, ECMAScriptimport/export statements are left alone in the.js output;when it’s compiled as a CommonJS module, it will produce the same output you get today under--module commonjs.

This also means paths resolve differently between.ts files that are ES modules and ones that are CJS modules.For example, let’s say you have the following code today:

ts
// ./foo.ts
exportfunctionhelper() {
// ...
}
// ./bar.ts
import {helper }from"./foo";// only works in CJS
helper();

This code works in CommonJS modules, but will fail in ES modules because relative import paths need to use extensions.As a result, it will have to be rewritten to use the extension of theoutput offoo.ts - sobar.ts will instead have to import from./foo.js.

ts
// ./bar.ts
import {helper }from"./foo.js";// works in ESM & CJS
helper();

This might feel a bit cumbersome at first, but TypeScript tooling like auto-imports and path completion will typically just do this for you.

One other thing to mention is the fact that this applies to.d.ts files too.When TypeScript finds a.d.ts file in a package, it is interpreted based on the containing package.

New File Extensions

Thetype field inpackage.json is nice because it allows us to continue using the.ts and.js file extensions which can be convenient;however, you will occasionally need to write a file that differs from whattype specifies.You might also just prefer to always be explicit.

Node.js supports two extensions to help with this:.mjs and.cjs..mjs files are always ES modules, and.cjs files are always CommonJS modules, and there’s no way to override these.

In turn, TypeScript supports two new source file extensions:.mts and.cts.When TypeScript emits these to JavaScript files, it will emit them to.mjs and.cjs respectively.

Furthermore, TypeScript also supports two new declaration file extensions:.d.mts and.d.cts.When TypeScript generates declaration files for.mts and.cts, their corresponding extensions will be.d.mts and.d.cts.

Using these extensions is entirely optional, but will often be useful even if you choose not to use them as part of your primary workflow.

CommonJS Interoperability

Node.js allows ES modules to import CommonJS modules as if they were ES modules with a default export.

ts
// ./foo.cts
exportfunctionhelper() {
console.log("hello world!");
}
// ./bar.mts
importfoofrom"./foo.cjs";
// prints "hello world!"
foo.helper();

In some cases, Node.js also synthesizes named exports from CommonJS modules, which can be more convenient.In these cases, ES modules can use a “namespace-style” import (i.e.import * as foo from "..."), or named imports (i.e.import { helper } from "...").

ts
// ./foo.cts
exportfunctionhelper() {
console.log("hello world!");
}
// ./bar.mts
import {helper }from"./foo.cjs";
// prints "hello world!"
helper();

There isn’t always a way for TypeScript to know whether these named imports will be synthesized, but TypeScript will err on being permissive and use some heuristics when importing from a file that is definitely a CommonJS module.

One TypeScript-specific note about interop is the following syntax:

ts
importfoo =require("foo");

In a CommonJS module, this just boils down to arequire() call, and in an ES module, this importscreateRequire to achieve the same thing.This will make code less portable on runtimes like the browser (which don’t supportrequire()), but will often be useful for interoperability.In turn, you can write the above example using this syntax as follows:

ts
// ./foo.cts
exportfunctionhelper() {
console.log("hello world!");
}
// ./bar.mts
importfoo =require("./foo.cjs");
foo.helper()

Finally, it’s worth noting that the only way to import ESM files from a CJS module is using dynamicimport() calls.This can present challenges, but is the behavior in Node.js today.

You canread more about ESM/CommonJS interop in Node.js here.

package.json Exports, Imports, and Self-Referencing

Node.js supportsa new field for defining entry points inpackage.json called"exports".This field is a more powerful alternative to defining"main" inpackage.json, and can control what parts of your package are exposed to consumers.

Here’s apackage.json that supports separate entry-points for CommonJS and ESM:

jsonc
// package.json
{
"name":"my-package",
"type":"module",
"exports": {
".": {
// Entry-point for `import "my-package"` in ESM
"import":"./esm/index.js",
// Entry-point for `require("my-package") in CJS
"require":"./commonjs/index.cjs",
},
},
// CJS fall-back for older versions of Node.js
"main":"./commonjs/index.cjs",
}

There’s a lot to this feature,which you can read more about on the Node.js documentation.Here we’ll try to focus on how TypeScript supports it.

With TypeScript’s original Node support, it would look for a"main" field, and then look for declaration files that corresponded to that entry.For example, if"main" pointed to./lib/index.js, TypeScript would look for a file called./lib/index.d.ts.A package author could override this by specifying a separate field called"types" (e.g."types": "./types/index.d.ts").

The new support works similarly withimport conditions.By default, TypeScript overlays the same rules with import conditions - if you write animport from an ES module, it will look up theimport field, and from a CommonJS module, it will look at therequire field.If it finds them, it will look for a corresponding declaration file.If you need to point to a different location for your type declarations, you can add a"types" import condition.

jsonc
// package.json
{
"name":"my-package",
"type":"module",
"exports": {
".": {
// Entry-point for `import "my-package"` in ESM
"import": {
// Where TypeScript will look.
"types":"./types/esm/index.d.ts",
// Where Node.js will look.
"default":"./esm/index.js"
},
// Entry-point for `require("my-package") in CJS
"require": {
// Where TypeScript will look.
"types":"./types/commonjs/index.d.cts",
// Where Node.js will look.
"default":"./commonjs/index.cjs"
},
}
},
// Fall-back for older versions of TypeScript
"types":"./types/index.d.ts",
// CJS fall-back for older versions of Node.js
"main":"./commonjs/index.cjs"
}

The"types" condition should always come first in"exports".

It’s important to note that the CommonJS entrypoint and the ES module entrypoint each needs its own declaration file, even if the contents are the same between them.Every declaration file is interpreted either as a CommonJS module or as an ES module, based on its file extension and the"type" field of thepackage.json, and this detected module kind must match the module kind that Node will detect for the corresponding JavaScript file for type checking to be correct.Attempting to use a single.d.ts file to type both an ES module entrypoint and a CommonJS entrypoint will cause TypeScript to think only one of those entrypoints exists, causing compiler errors for users of the package.

TypeScript also supportsthe"imports" field ofpackage.json in a similar manner by looking for declaration files alongside corresponding files, and supportspackages self-referencing themselves.These features are generally not as involved to set up, but are supported.

Your Feedback Wanted!

As we continue working on TypeScript 4.7, we expect to see more documentation and polish go into this functionality.Supporting these new features has been an ambitious under-taking, and that’s why we’re looking for early feedback on it!Please try it out and let us know how it works for you.

For more information,you can see the implementing PR here.

Control over Module Detection

One issue with the introduction of modules to JavaScript was the ambiguity between existing “script” code and the new module code.JavaScript code in a module runs slightly differently, and has different scoping rules, so tools have to make decisions as to how each file runs.For example, Node.js requires module entry-points to be written in a.mjs, or have a nearbypackage.json with"type": "module".TypeScript treats a file as a module whenever it finds anyimport orexport statement in a file, but otherwise, will assume a.ts or.js file is a script file acting on the global scope.

This doesn’t quite match up with the behavior of Node.js where thepackage.json can change the format of a file, or the--jsx settingreact-jsx, where any JSX file contains an implicit import to a JSX factory.It also doesn’t match modern expectations where most new TypeScript code is written with modules in mind.

That’s why TypeScript 4.7 introduces a new option calledmoduleDetection.moduleDetection can take on 3 values:"auto" (the default),"legacy" (the same behavior as 4.6 and prior), and"force".

Under the mode"auto", TypeScript will not only look forimport andexport statements, but it will also check whether

  • the"type" field inpackage.json is set to"module" when running under--module nodenext/--module node16, and
  • check whether the current file is a JSX file when running under--jsx react-jsx

In cases where you want every file to be treated as a module, the"force" setting ensures that every non-declaration file is treated as a module.This will be true regardless of howmodule,moduleResolution, andjsx are configured.

Meanwhile, the"legacy" option simply goes back to the old behavior of only seeking outimport andexport statements to determine whether a file is a module.

You canread up more about this change on the pull request.

Control-Flow Analysis for Bracketed Element Access

TypeScript 4.7 now narrows the types of element accesses when the indexed keys are literal types and unique symbols.For example, take the following code:

ts
constkey =Symbol();
constnumberOrString =Math.random() <0.5 ?42 :"hello";
constobj = {
[key]:numberOrString,
};
if (typeofobj[key] ==="string") {
letstr =obj[key].toUpperCase();
}

Previously, TypeScript would not consider any type guards onobj[key], and would have no idea thatobj[key] was really astring.Instead, it would think thatobj[key] was still astring | number and accessingtoUpperCase() would trigger an error.

TypeScript 4.7 now knows thatobj[key] is a string.

This also means that under--strictPropertyInitialization, TypeScript can correctly check that computed properties are initialized by the end of a constructor body.

ts
// 'key' has type 'unique symbol'
constkey =Symbol();
classC {
[key]:string;
constructor(str:string) {
// oops, forgot to set 'this[key]'
}
screamString() {
returnthis[key].toUpperCase();
}
}

Under TypeScript 4.7,--strictPropertyInitialization reports an error telling us that the[key] property wasn’t definitely assigned by the end of the constructor.

We’d like to extend our gratitude toOleksandr Tarasiuk who providedthis change!

Improved Function Inference in Objects and Methods

TypeScript 4.7 can now perform more granular inferences from functions within objects and arrays.This allows the types of these functions to consistently flow in a left-to-right manner just like for plain arguments.

ts
declarefunctionf<T>(arg: {
produce: (n:string)=>T,
consume: (x:T)=>void }
):void;
// Works
f({
produce: ()=>"hello",
consume:x=>x.toLowerCase()
});
// Works
f({
produce: (n:string)=>n,
consume:x=>x.toLowerCase(),
});
// Was an error, now works.
f({
produce:n=>n,
consume:x=>x.toLowerCase(),
});
// Was an error, now works.
f({
produce:function () {return"hello"; },
consume:x=>x.toLowerCase(),
});
// Was an error, now works.
f({
produce() {return"hello" },
consume:x=>x.toLowerCase(),
});

Inference failed in some of these examples because knowing the type of theirproduce functions would indirectly request the type ofarg before finding a good type forT.TypeScript now gathers functions that could contribute to the inferred type ofT and infers from them lazily.

For more information, you cantake a look at the specific modifications to our inference process.

Instantiation Expressions

Occasionally functions can be a bit more general than we want.For example, let’s say we had amakeBox function.

ts
interfaceBox<T> {
value:T;
}
functionmakeBox<T>(value:T) {
return {value };
}

Maybe we want to create a more specialized set of functions for makingBoxes ofWrenches andHammers.To do that today, we’d have to wrapmakeBox in other functions, or use an explicit type for an alias ofmakeBox.

ts
functionmakeHammerBox(hammer:Hammer) {
returnmakeBox(hammer);
}
// or...
constmakeWrenchBox: (wrench:Wrench)=>Box<Wrench> =makeBox;

These work, but wrapping a call tomakeBox is a bit wasteful, and writing the full signature ofmakeWrenchBox could get unwieldy.Ideally, we would be able to say that we just want to aliasmakeBox while replacing all of the generics in its signature.

TypeScript 4.7 allows exactly that!We can now take functions and constructors and feed them type arguments directly.

ts
constmakeHammerBox =makeBox<Hammer>;
constmakeWrenchBox =makeBox<Wrench>;

So with this, we can specializemakeBox to accept more specific types and reject anything else.

ts
constmakeStringBox =makeBox<string>;
// TypeScript correctly rejects this.
makeStringBox(42);

This logic also works for constructor functions such asArray,Map, andSet.

ts
// Has type `new () => Map<string, Error>`
constErrorMap =Map<string,Error>;
// Has type `// Map<string, Error>`
consterrorMap =newErrorMap();

When a function or constructor is given type arguments, it will produce a new type that keeps all signatures with compatible type parameter lists, and replaces the corresponding type parameters with the given type arguments.Any other signatures are dropped, as TypeScript will assume that they aren’t meant to be used.

For more information on this feature,check out the pull request.

extends Constraints oninfer Type Variables

Conditional types are a bit of a power-user feature.They allow us to match and infer against the shape of types, and make decisions based on them.For example, we can write a conditional type that returns the first element of a tuple type if it’s astring-like type.

ts
typeFirstIfString<T> =
Textends [inferS, ...unknown[]]
?Sextendsstring ?S :never
:never;
// string
typeA =FirstIfString<[string,number,number]>;
// "hello"
typeB =FirstIfString<["hello",number,number]>;
// "hello" | "world"
typeC =FirstIfString<["hello" |"world",boolean]>;
// never
typeD =FirstIfString<[boolean,number,string]>;

FirstIfString matches against any tuple with at least one element and grabs the type of the first element asS.Then it checks ifS is compatible withstring and returns that type if it is.

Note that we had to use two conditional types to write this.We could have writtenFirstIfString as follows:

ts
typeFirstIfString<T> =
Textends [string, ...unknown[]]
// Grab the first type out of `T`
?T[0]
:never;

This works, but it’s slightly more “manual” and less declarative.Instead of just pattern-matching on the type and giving the first element a name, we have to fetch out the0th element ofT withT[0].If we were dealing with types more complex than tuples, this could get a lot trickier, soinfer can simplify things.

Using nested conditionals to infer a type and then match against that inferred type is pretty common.To avoid that second level of nesting, TypeScript 4.7 now allows you to place a constraint on anyinfer type.

ts
typeFirstIfString<T> =
Textends [inferSextendsstring, ...unknown[]]
?S
:never;

This way, when TypeScript matches againstS, it also ensures thatS has to be astring.IfS isn’t astring, it takes the false path, which in these cases isnever.

For more details, you canread up on the change on GitHub.

Optional Variance Annotations for Type Parameters

Let’s take the following types.

ts
interfaceAnimal {
animalStuff:any;
}
interfaceDogextendsAnimal {
dogStuff:any;
}
// ...
typeGetter<T> = ()=>T;
typeSetter<T> = (value:T)=>void;

Imagine we had two different instances ofGetters.Figuring out whether any two differentGetters are substitutable for one another depends entirely onT.In the case of whether an assignment ofGetter<Dog> → Getter<Animal> is valid, we have to check whetherDog → Animal is valid.Because each type forT just gets related in the same “direction”, we say that theGetter type iscovariant onT.On the other hand, checking whetherSetter<Dog> → Setter<Animal> is valid involves checking whetherAnimal → Dog is valid.That “flip” in direction is kind of like how in math, checking whether −x < −y is the same as checking whethery < x.When we have to flip directions like this to compareT, we say thatSetter iscontravariant onT.

With TypeScript 4.7, we’re now able toexplicitly specify variance on type parameters.

So now, if we want to make it explicit thatGetter is covariant onT, we can now give it anout modifier.

ts
typeGetter<outT> = ()=>T;

And similarly, if we also want to make it explicit thatSetter is contravariant onT, we can give it anin modifier.

ts
typeSetter<inT> = (value:T)=>void;

out andin are used here because a type parameter’s variance depends on whether it’s used in anoutput or aninput.Instead of thinking about variance, you can just think about ifT is used in output and input positions.

There are also cases for using bothin andout.

ts
interfaceState<inoutT> {
get: ()=>T;
set: (value:T)=>void;
}

When aT is used in both an output and input position, it becomesinvariant.Two differentState<T>s can’t be interchanged unless theirTs are the same.In other words,State<Dog> andState<Animal> aren’t substitutable for the other.

Now technically speaking, in a purely structural type system, type parameters and their variance don’t really matter - you can just plug in types in place of each type parameter and check whether each matching member is structurally compatible.So if TypeScript uses a structural type system, why are we interested in the variance of type parameters?And why might we ever want to annotate them?

One reason is that it can be useful for a reader to explicitly see how a type parameter is used at a glance.For much more complex types, it can be difficult to tell whether a type is meant to be read, written, or both.TypeScript will also help us out if we forget to mention how that type parameter is used.As an example, if we forgot to specify bothin andout onState, we’d get an error.

ts
interfaceState<outT> {
// ~~~~~
// error!
// Type 'State<sub-T>' is not assignable to type 'State<super-T>' as implied by variance annotation.
// Types of property 'set' are incompatible.
// Type '(value: sub-T) => void' is not assignable to type '(value: super-T) => void'.
// Types of parameters 'value' and 'value' are incompatible.
// Type 'super-T' is not assignable to type 'sub-T'.
get: ()=>T;
set: (value:T)=>void;
}

Another reason is precision and speed!TypeScript already tries to infer the variance of type parameters as an optimization.By doing this, it can type-check larger structural types in a reasonable amount of time.Calculating variance ahead of time allows the type-checker to skip deeper comparisons and just compare type arguments which can bemuch faster than comparing the full structure of a type over and over again.But often there are cases where this calculation is still fairly expensive, and the calculation may find circularities that can’t be accurately resolved, meaning there’s no clear answer for the variance of a type.

ts
typeFoo<T> = {
x:T;
f:Bar<T>;
}
typeBar<U> = (x:Baz<U[]>)=>void;
typeBaz<V> = {
value:Foo<V[]>;
}
declareletfoo1:Foo<unknown>;
declareletfoo2:Foo<string>;
foo1 =foo2;// Should be an error but isn't ❌
foo2 =foo1;// Error - correct ✅

Providing an explicit annotation can speed up type-checking at these circularities and provide better accuracy.For instance, markingT as invariant in the above example can help stop the problematic assignment.

diff
- type Foo<T> = {
+ type Foo<in out T> = {
x: T;
f: Bar<T>;
}

We don’t necessarily recommend annotating every type parameter with its variance;For example, it’s possible (but not recommended) to make variance a little stricter than is necessary, so TypeScript won’t stop you from marking something as invariant if it’s really just covariant, contravariant, or even independent.So if you do choose to add explicit variance markers, we would encourage thoughtful and precise use of them.

But if you’re working with deeply recursive types, especially if you’re a library author, you may be interested in using these annotations to the benefit of your users.Those annotations can provide wins in both accuracy and type-checking speed, which can even affect their code editing experience.Determining when variance calculation is a bottleneck on type-checking time can be done experimentally, and determined using tooling like ouranalyze-trace utility.

For more details on this feature, you canread up on the pull request.

Resolution Customization withmoduleSuffixes

TypeScript 4.7 now supports amoduleSuffixes option to customize how module specifiers are looked up.

jsonc
{
"compilerOptions": {
"moduleSuffixes": [".ios",".native",""]
}
}

Given the above configuration, an import like the following…

ts
import*asfoofrom"./foo";

will try to look at the relative files./foo.ios.ts,./foo.native.ts, and finally./foo.ts.

Note that the empty string"" inmoduleSuffixes is necessary for TypeScript to also look-up./foo.ts.In a sense, the default value formoduleSuffixes is[""].

This feature can be useful for React Native projects where each target platform can use a separatetsconfig.json with differingmoduleSuffixes.

ThemoduleSuffixes option was contributed thanks toAdam Foxman!

resolution-mode

With Node’s ECMAScript resolution, the mode of the containing file and the syntax you use determines how imports are resolved;however it would be useful to reference the types of a CommonJS module from an ECMAScript module, or vice-versa.

TypeScript now allows/// <reference types="..." /> directives.

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

Additionally, in nightly versions of TypeScript,import type can specify an import assertion to achieve something similar.

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

These import assertions can also be used onimport() types.

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

Theimport type andimport() syntaxes only supportresolution-mode innightly builds of TypeScript.You’ll likely get an error like

Resolution mode assertions are unstable. Use nightly TypeScript to silence this error. Try updating with 'npm install -D typescript@next'.

If you do find yourself using this feature in nightly versions of TypeScript,consider providing feedback on this issue.

You can see the respective changesfor reference directives andfor type import assertions.

Go to Source Definition

TypeScript 4.7 contains support for a new experimental editor command calledGo To Source Definition.It’s similar toGo To Definition, but it never returns results inside declaration files.Instead, it tries to find correspondingimplementation files (like.js or.ts files), and find definitions there — even if those files are normally shadowed by.d.ts files.

This comes in handy most often when you need to peek at the implementation of a function you’re importing from a library instead of its type declaration in a.d.ts file.

The "Go to Source Definition" command on a use of the yargs package jumps the editor to an index.cjs file in yargs.

You can try this new command in the latest versions of Visual Studio Code.Note, though, that this functionality is still in preview, and there are some known limitations.In some cases TypeScript uses heuristics to guess which.js file corresponds to the given result of a definition, so these results might be inaccurate.Visual Studio Code also doesn’t yet indicate whether a result was a guess, but it’s something we’re collaborating on.

You can leave feedback about the feature, read about known limitations, or learn more atour dedicated feedback issue.

Group-Aware Organize Imports

TypeScript has anOrganize Imports editor feature for both JavaScript and TypeScript.Unfortunately, it could be a bit of a blunt instrument, and would often naively sort your import statements.

For instance, if you ran Organize Imports on the following file…

ts
// local code
import*asbbbfrom"./bbb";
import*ascccfrom"./ccc";
import*asaaafrom"./aaa";
// built-ins
import*aspathfrom"path";
import*aschild_processfrom"child_process"
import*asfsfrom"fs";
// some code...

You would get something like the following

ts
// local code
import*aschild_processfrom"child_process";
import*asfsfrom"fs";
// built-ins
import*aspathfrom"path";
import*asaaafrom"./aaa";
import*asbbbfrom"./bbb";
import*ascccfrom"./ccc";
// some code...

This is… not ideal.Sure, our imports are sorted by their paths, and our comments and newlines are preserved, but not in a way we expected.Much of the time, if we have our imports grouped in a specific way, then we want to keep them that way.

TypeScript 4.7 performs Organize Imports in a group-aware manner.Running it on the above code looks a little bit more like what you’d expect:

ts
// local code
import*asaaafrom"./aaa";
import*asbbbfrom"./bbb";
import*ascccfrom"./ccc";
// built-ins
import*aschild_processfrom"child_process";
import*asfsfrom"fs";
import*aspathfrom"path";
// some code...

We’d like to extend our thanks toMinh Quy who providedthis feature.

Object Method Snippet Completions

TypeScript now provides snippet completions for object literal methods.When completing members in an object, TypeScript will provide a typical completion entry for just the name of a method, along with a separate completion entry for the full method definition!

Completion a full method signature from an object

For more details,see the implementing pull request.

Breaking Changes

lib.d.ts Updates

While TypeScript strives to avoid major breaks, even small changes in the built-in libraries can cause issues.We don’t expect major breaks as a result of DOM andlib.d.ts updates, but there may be some small ones.

Stricter Spread Checks in JSX

When writing a...spread in JSX, TypeScript now enforces stricter checks that the given type is actually an object.As a result, values with the typesunknown andnever (and more rarely, just barenull andundefined) can no longer be spread into JSX elements.

So for the following example:

tsx
import*asReactfrom"react";
interfaceProps {
stuff?:string;
}
functionMyComponent(props:unknown) {
return<div{...props}/>;
}

you’ll now receive an error like the following:

Spread types may only be created from object types.

This makes this behavior more consistent with spreads in object literals.

For more details,see the change on GitHub.

Stricter Checks with Template String Expressions

When asymbol value is used in a template string, it will trigger a runtime error in JavaScript.

js
letstr =`hello${Symbol()}`;
// TypeError: Cannot convert a Symbol value to a string

As a result, TypeScript will issue an error as well;however, TypeScript now also checks if a generic value that is constrained to a symbol in some way is used in a template string.

ts
functionlogKey<Sextendsstring |symbol>(key:S):S {
// Now an error.
console.log(`${key} is the key`);
returnkey;
}
functionget<T,KextendskeyofT>(obj:T,key:K) {
// Now an error.
console.log(`Grabbing property '${key}'.`);
returnobj[key];
}

TypeScript will now issue the following error:

Implicit conversion of a 'symbol' to a 'string' will fail at runtime. Consider wrapping this expression in 'String(...)'.

In some cases, you can get around this by wrapping the expression in a call toString, just like the error message suggests.

ts
functionlogKey<Sextendsstring |symbol>(key:S):S {
// No longer an error.
console.log(`${String(key)} is the key`);
returnkey;
}

In others, this error is too pedantic, and you might not ever care to even allowsymbol keys when usingkeyof.In such cases, you can switch tostring & keyof ...:

ts
functionget<T,Kextendsstring &keyofT>(obj:T,key:K) {
// No longer an error.
console.log(`Grabbing property '${key}'.`);
returnobj[key];
}

For more information, you cansee the implementing pull request.

readFile Method is No Longer Optional onLanguageServiceHost

If you’re creatingLanguageService instances, then providedLanguageServiceHosts will need to provide areadFile method.This change was necessary to support the newmoduleDetection compiler option.

You canread more on the change here.

readonly Tuples Have areadonlylength Property

Areadonly tuple will now treat itslength property asreadonly.This was almost never witnessable for fixed-length tuples, but was an oversight which could be observed for tuples with trailing optional and rest element types.

As a result, the following code will now fail:

ts
functionoverwriteLength(tuple:readonly [string,string,string]) {
// Now errors.
tuple.length =7;
}

You canread more on this change here.

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

Contributors to this page:
ABAndrew Branch  (9)
NTNémeth Tamás  (1)
PADPBPedro Augusto de Paula Barbosa  (1)
HCHyunyoung Cho  (1)

Last updated: Dec 16, 2025