Was this page helpful?

TypeScript 5.1

Easier Implicit Returns forundefined-Returning Functions

In JavaScript, if a function finishes running without hitting areturn, it returns the valueundefined.

ts
functionfoo() {
// no return
}
// x = undefined
letx =foo();

However, in previous versions of TypeScript, theonly functions that could have absolutely no return statements werevoid- andany-returning functions.That meant that even if you explicitly said “this function returnsundefined” you were forced to have at least one return statement.

ts
// ✅ fine - we inferred that 'f1' returns 'void'
functionf1() {
// no returns
}
// ✅ fine - 'void' doesn't need a return statement
functionf2():void {
// no returns
}
// ✅ fine - 'any' doesn't need a return statement
functionf3():any {
// no returns
}
// ❌ error!
// A function whose declared type is neither 'void' nor 'any' must return a value.
functionf4():undefined {
// no returns
}

This could be a pain if some API expected a function returningundefined - you would need to have either at least one explicit return ofundefined or areturn statementand an explicit annotation.

ts
declarefunctiontakesFunction(f: ()=>undefined):undefined;
// ❌ error!
// Argument of type '() => void' is not assignable to parameter of type '() => undefined'.
takesFunction(()=> {
// no returns
});
// ❌ error!
// A function whose declared type is neither 'void' nor 'any' must return a value.
takesFunction(():undefined=> {
// no returns
});
// ❌ error!
// Argument of type '() => void' is not assignable to parameter of type '() => undefined'.
takesFunction(()=> {
return;
});
// ✅ works
takesFunction(()=> {
returnundefined;
});
// ✅ works
takesFunction(():undefined=> {
return;
});

This behavior was frustrating and confusing, especially when calling functions outside of one’s control.Understanding the interplay between inferringvoid overundefined, whether anundefined-returning function needs areturn statement, etc. seems like a distraction.

First, TypeScript 5.1 now allowsundefined-returning functions to have no return statement.

ts
// ✅ Works in TypeScript 5.1!
functionf4():undefined {
// no returns
}
// ✅ Works in TypeScript 5.1!
takesFunction(():undefined=> {
// no returns
});

Second, if a function has no return expressions and is being passed to something expecting a function that returnsundefined, TypeScript infersundefined for that function’s return type.

ts
// ✅ Works in TypeScript 5.1!
takesFunction(functionf() {
// ^ return type is undefined
// no returns
});
// ✅ Works in TypeScript 5.1!
takesFunction(functionf() {
// ^ return type is undefined
return;
});

To address another similar pain-point, under TypeScript’s--noImplicitReturns option, functions returningonlyundefined now have a similar exception tovoid, in that not every single code path must end in an explicitreturn.

ts
// ✅ Works in TypeScript 5.1 under '--noImplicitReturns'!
functionf():undefined {
if (Math.random()) {
// do some stuff...
return;
}
}

For more information, you can read up onthe original issue andthe implementing pull request.

Unrelated Types for Getters and Setters

TypeScript 4.3 made it possible to say that aget andset accessor pair might specify two different types.

ts
interfaceSerializer {
setvalue(v:string |number |boolean);
getvalue():string;
}
declareletbox:Serializer;
// Allows writing a 'boolean'
box.value =true;
// Comes out as a 'string'
console.log(box.value.toUpperCase());

Initially we required that theget type had to be a subtype of theset type.This meant that writing

ts
box.value =box.value;

would always be valid.

However, there are plenty of existing and proposed APIs that have completely unrelated types between their getters and setters.For example, consider one of the most common examples - thestyle property in the DOM andCSSStyleRule API.Every style rule hasastyle property that is aCSSStyleDeclaration;however, if you try to write to that property, it will only work correctly with a string!

TypeScript 5.1 now allows completely unrelated types forget andset accessor properties, provided that they have explicit type annotations.And while this version of TypeScript does not yet change the types for these built-in interfaces,CSSStyleRule can now be defined in the following way:

ts
interfaceCSSStyleRule {
// ...
/** Always reads as a `CSSStyleDeclaration` */
getstyle():CSSStyleDeclaration;
/** Can only write a `string` here. */
setstyle(newValue:string);
// ...
}

This also allows other patterns like requiringset accessors to accept only “valid” data, but specifying thatget accessors may returnundefined if some underlying state hasn’t been initialized yet.

ts
classSafeBox {
#value:string |undefined;
// Only accepts strings!
setvalue(newValue:string) {
}
// Must check for 'undefined'!
getvalue():string |undefined {
returnthis.#value;
}
}

In fact, this is similar to how optional properties are checked under--exactOptionalProperties.

You can read up more onthe implementing pull request.

Decoupled Type-Checking Between JSX Elements and JSX Tag Types

One pain point TypeScript had with JSX was its requirements on the type of every JSX element’s tag.

For context, a JSX element is either of the following:

tsx
// A self-closing JSX tag
<Foo/>
// A regular element with an opening/closing tag
<Bar></Bar>

When type-checking<Foo /> or<Bar></Bar>, TypeScript always looks up a namespace calledJSX and fetches a type out of it calledElement - or more directly, it looks upJSX.Element.

But to check whetherFoo orBar themselves were valid to use as tag names, TypeScript would roughly just grab the types returned or constructed byFoo orBar and check for compatibility withJSX.Element (or another type calledJSX.ElementClass if the type is constructable).

The limitations here meant that components could not be used if they returned or “rendered” a more broad type than justJSX.Element.For example, a JSX library might be fine with a component returningstrings orPromises.

As a more concrete example,React is considering adding limited support for components that returnPromises, but existing versions of TypeScript cannot express that without someone drastically loosening the type ofJSX.Element.

tsx
import*asReactfrom"react";
asyncfunctionFoo() {
return<div></div>;
}
letelement =<Foo/>;
// ~~~
// 'Foo' cannot be used as a JSX component.
// Its return type 'Promise<Element>' is not a valid JSX element.

To provide libraries with a way to express this, TypeScript 5.1 now looks up a type calledJSX.ElementType.ElementType specifies precisely what is valid to use as a tag in a JSX element.So it might be typed today as something like

tsx
namespaceJSX {
exporttypeElementType =
// All the valid lowercase tags
keyofIntrinsicAttributes
// Function components
(props:any)=>Element
// Class components
new (props:any)=>ElementClass;
exportinterfaceIntrinsicAttributesextends/*...*/ {}
exporttypeElement =/*...*/;
exporttypeElementClass =/*...*/;
}

We’d like to extend our thanks toSebastian Silbermann who contributedthis change!

Namespaced JSX Attributes

TypeScript now supports namespaced attribute names when using JSX.

tsx
import*asReactfrom"react";
// Both of these are equivalent:
constx =<Fooa:b="hello"/>;
consty =<Fooa:b="hello"/>;
interfaceFooProps {
"a:b":string;
}
functionFoo(props:FooProps) {
return<div>{props["a:b"]}</div>;
}

Namespaced tag names are looked up in a similar way onJSX.IntrinsicAttributes when the first segment of the name is a lowercase name.

tsx
// In some library's code or in an augmentation of that library:
namespaceJSX {
interfaceIntrinsicElements {
["a:b"]: {prop:string };
}
}
// In our code:
letx =<a:bprop="hello!"/>;

This contribution was provided thanks toOleksandr Tarasiuk.

typeRoots Are Consulted In Module Resolution

When TypeScript’s specified module lookup strategy is unable to resolve a path, it will now resolve packages relative to the specifiedtypeRoots.

Seethis pull request for more details.

Move Declarations to Existing Files

In addition to moving declarations to new files, TypeScript now ships a preview feature for moving declarations to existing files as well.You can try this functionality out in a recent version of Visual Studio Code.

Moving a function 'getThanks' to an existing file in the workspace.

Keep in mind that this feature is currently in preview, and we are seeking further feedback on it.

https://github.com/microsoft/TypeScript/pull/53542

Linked Cursors for JSX Tags

TypeScript now supportslinked editing for JSX tag names.Linked editing (occasionally called “mirrored cursors”) allows an editor to edit multiple locations at the same time automatically.

An example of JSX tags with linked editing modifying a JSX fragment and a div element.

This new feature should work in both TypeScript and JavaScript files, and can be enabled in Visual Studio Code Insiders.In Visual Studio Code, you can either edit theEditor: Linked Editing option in the Settings UI:

Visual Studio Code's Editor: Linked Editing` option

or configureeditor.linkedEditing in your JSON settings file:

jsonc
{
// ...
"editor.linkedEditing":true,
}

This feature will also be supported by Visual Studio 17.7 Preview 1.

You can take a look atour implementation of linked editing here!

Snippet Completions for@param JSDoc Tags

TypeScript now provides snippet completions when typing out a@param tag in both TypeScript and JavaScript files.This can help cut down on some typing and jumping around text as you document your code or add JSDoc types in JavaScript.

An example of completing JSDoc param comments on an 'add' function.

You cancheck out how this new feature was implemented on GitHub.

Optimizations

Avoiding Unnecessary Type Instantiation

TypeScript 5.1 now avoids performing type instantiation within object types that are known not to contain references to outer type parameters.This has the potential to cut down on many unnecessary computations, and reduced the type-checking time ofmaterial-ui’s docs directory by over 50%.

You cansee the changes involved for this change on GitHub.

Negative Case Checks for Union Literals

When checking if a source type is part of a union type, TypeScript will first do a fast look-up using an internal type identifier for that source.If that look-up fails, then TypeScript checks for compatibility against every type within the union.

When relating a literal type to a union of purely literal types, TypeScript can now avoid that full walk against every other type in the union.This assumption is safe because TypeScript always interns/caches literal types - though there are some edge cases to handle relating to “fresh” literal types.

This optimization was able to reduce the type-checking time ofthe code in this issue from about 45 seconds to about 0.4 seconds.

Reduced Calls into Scanner for JSDoc Parsing

When older versions of TypeScript parsed out a JSDoc comment, they would use the scanner/tokenizer to break the comment into fine-grained tokens and piece the contents back together.This could be helpful for normalizing comment text, so that multiple spaces would just collapse into one;but it was extremely “chatty” and meant the parser and scanner would jump back and forth very often, adding overhead to JSDoc parsing.

TypeScript 5.1 has moved more logic around breaking down JSDoc comments into the scanner/tokenizer.The scanner now returns larger chunks of content directly to the parser to do as it needs.

These changes have brought down the parse time of several 10Mb mostly-prose-comment JavaScript files by about half.For a more realistic example, our performance suite’s snapshot ofxstate dropped about 300ms of parse time, making it faster to load and analyze.

Breaking Changes

ES2020 and Node.js 14.17 as Minimum Runtime Requirements

TypeScript 5.1 now ships JavaScript functionality that was introduced in ECMAScript 2020.As a result, at minimum TypeScript must be run in a reasonably modern runtime.For most users, this means TypeScript now only runs on Node.js 14.17 and later.

If you try running TypeScript 5.1 under an older version of Node.js such as Node 10 or 12, you may see an error like the following from running eithertsc.js ortsserver.js:

node_modules/typescript/lib/tsserver.js:2406
for (let i = startIndex ?? 0; i < array.length; i++) {
^
SyntaxError: Unexpected token '?'
at wrapSafe (internal/modules/cjs/loader.js:915:16)
at Module._compile (internal/modules/cjs/loader.js:963:27)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
at Module.load (internal/modules/cjs/loader.js:863:32)
at Function.Module._load (internal/modules/cjs/loader.js:708:14)
at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
at internal/main/run_main_module.js:17:47

Additionally, if you try installing TypeScript you’ll get something like the following error messages from npm:

npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE package: 'typescript@5.1.1-rc',
npm WARN EBADENGINE required: { node: '>=14.17' },
npm WARN EBADENGINE current: { node: 'v12.22.12', npm: '8.19.2' }
npm WARN EBADENGINE }

from Yarn:

error typescript@5.1.1-rc: The engine "node" is incompatible with this module. Expected version ">=14.17". Got "12.22.12"
error Found incompatible module.

See more information around this change here.

ExplicittypeRoots Disables Upward Walks fornode_modules/@types

Previously, when thetypeRoots option was specified in atsconfig.json but resolution to anytypeRoots directories had failed, TypeScript would still continue walking up parent directories, trying to resolve packages within each parent’snode_modules/@types folder.

This behavior could prompt excessive look-ups and has been disabled in TypeScript 5.1.As a result, you may begin to see errors like the following based on entries in yourtsconfig.json’stypes option or/// <reference > directives

error TS2688: Cannot find type definition file for 'node'.
error TS2688: Cannot find type definition file for 'mocha'.
error TS2688: Cannot find type definition file for 'jasmine'.
error TS2688: Cannot find type definition file for 'chai-http'.
error TS2688: Cannot find type definition file for 'webpack-env"'.

The solution is typically to add specific entries fornode_modules/@types to yourtypeRoots:

jsonc
{
"compilerOptions": {
"types": [
"node",
"mocha"
],
"typeRoots": [
// Keep whatever you had around before.
"./some-custom-types/",
// You might need your local 'node_modules/@types'.
"./node_modules/@types",
// You might also need to specify a shared 'node_modules/@types'
// if you're using a "monorepo" layout.
"../../node_modules/@types",
]
}
}

More information is availableon the original change on our issue tracker.

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

Contributors to this page:
Nnavya9singh  (6)
BTBeeno Tung  (1)
LLLazar Ljubenović  (1)

Last updated: Nov 25, 2025