Was this page helpful?

TypeScript 4.4

Control Flow Analysis of Aliased Conditions and Discriminants

In JavaScript, we often have to probe a value in different ways, and do something different once we know more about its type.TypeScript understands these checks and calls themtype guards.Instead of having to convince TypeScript of a variable’s type whenever we use it, the type-checker leverages something calledcontrol flow analysis to see if we’ve used a type guard before a given piece of code.

For example, we can write something like

ts
functionfoo(arg:unknown) {
if (typeofarg ==="string") {
console.log(arg.toUpperCase());
(parameter) arg: string
}
}
Try

In this example, we checked whetherarg was astring.TypeScript recognized thetypeof arg === "string" check, which it considered a type guard, and knew thatarg was astring inside the body of theif block.That let us accessstring methods liketoUpperCase() without getting an error.

However, what would happen if we moved the condition out to a constant calledargIsString?

ts
// In TS 4.3 and below
functionfoo(arg:unknown) {
constargIsString =typeofarg ==="string";
if (argIsString) {
console.log(arg.toUpperCase());
// ~~~~~~~~~~~
// Error! Property 'toUpperCase' does not exist on type 'unknown'.
}
}

In previous versions of TypeScript, this would be an error - even thoughargIsString was assigned the value of a type guard, TypeScript simply lost that information.That’s unfortunate since we might want to re-use the same check in several places.To get around that, users often have to repeat themselves or use type assertions (a.k.a. casts).

In TypeScript 4.4, that is no longer the case.The above example works with no errors!When TypeScript sees that we are testing a constant value, it will do a little bit of extra work to see if it contains a type guard.If that type guard operates on aconst, areadonly property, or an un-modified parameter, then TypeScript is able to narrow that value appropriately.

Different sorts of type guard conditions are preserved - not justtypeof checks.For example, checks on discriminated unions work like a charm.

ts
typeShape =
| {kind:"circle";radius:number }
| {kind:"square";sideLength:number };
 
functionarea(shape:Shape):number {
constisCircle =shape.kind ==="circle";
if (isCircle) {
// We know we have a circle here!
returnMath.PI *shape.radius **2;
}else {
// We know we're left with a square here!
returnshape.sideLength **2;
}
}
Try

Analysis on discriminants in 4.4 also goes a little bit deeper - we can now extract out discriminants and TypeScript can narrow the original object.

ts
typeShape =
| {kind:"circle";radius:number }
| {kind:"square";sideLength:number };
 
functionarea(shape:Shape):number {
// Extract out the 'kind' field first.
const {kind } =shape;
 
if (kind ==="circle") {
// We know we have a circle here!
returnMath.PI *shape.radius **2;
}else {
// We know we're left with a square here!
returnshape.sideLength **2;
}
}
Try

As another example, here’s a function that checks whether two of its inputs have contents.

ts
functiondoSomeChecks(
inputA:string |undefined,
inputB:string |undefined,
shouldDoExtraWork:boolean
) {
constmustDoWork =inputA &&inputB &&shouldDoExtraWork;
if (mustDoWork) {
// We can access 'string' properties on both 'inputA' and 'inputB'!
constupperA =inputA.toUpperCase();
constupperB =inputB.toUpperCase();
// ...
}
}
Try

TypeScript can understand that bothinputA andinputB are both present ifmustDoWork istrue.That means we don’t have to write a non-null assertion likeinputA! to convince TypeScript thatinputA isn’tundefined.

One neat feature here is that this analysis works transitively.TypeScript will hop through constants to understand what sorts of checks you’ve already performed.

ts
functionf(x:string |number |boolean) {
constisString =typeofx ==="string";
constisNumber =typeofx ==="number";
constisStringOrNumber =isString ||isNumber;
if (isStringOrNumber) {
x;
(parameter) x: string | number
}else {
x;
(parameter) x: boolean
}
}
Try

Note that there’s a cutoff - TypeScript doesn’t go arbitrarily deep when checking these conditions, but its analysis is deep enough for most checks.

This feature should make a lot of intuitive JavaScript code “just work” in TypeScript without it getting in your way.For more details,check out the implementation on GitHub!

Symbol and Template String Pattern Index Signatures

TypeScript lets us describe objects where every property has to have a certain type usingindex signatures.This allows us to use these objects as dictionary-like types, where we can use string keys to index into them with square brackets.

For example, we can write a type with an index signature that takesstring keys and maps toboolean values.If we try to assign anything other than aboolean value, we’ll get an error.

ts
interfaceBooleanDictionary {
[key:string]:boolean;
}
 
declareletmyDict:BooleanDictionary;
 
// Valid to assign boolean values
myDict["foo"] =true;
myDict["bar"] =false;
 
// Error, "oops" isn't a boolean
myDict["baz"] ="oops";
Type 'string' is not assignable to type 'boolean'.2322Type 'string' is not assignable to type 'boolean'.
Try

WhileaMap might be a better data structure here (specifically, aMap<string, boolean>), JavaScript objects are often more convenient to use or just happen to be what we’re given to work with.

Similarly,Array<T> already defines anumber index signature that lets us insert/retrieve values of typeT.

ts
// @errors: 2322 2375
// This is part of TypeScript's definition of the built-in Array type.
interfaceArray<T> {
[index:number]:T;
// ...
}
letarr =newArray<string>();
// Valid
arr[0] ="hello!";
// Error, expecting a 'string' value here
arr[1] =123;

Index signatures are very useful to express lots of code out in the wild;however, until now they’ve been limited tostring andnumber keys (andstring index signatures have an intentional quirk where they can acceptnumber keys since they’ll be coerced to strings anyway).That means that TypeScript didn’t allow indexing objects withsymbol keys.TypeScript also couldn’t model an index signature of somesubset ofstring keys - for example, an index signature which describes just properties whose names start with the textdata-.

TypeScript 4.4 addresses these limitations, and allows index signatures forsymbols and template string patterns.

For example, TypeScript now allows us to declare a type that can be keyed on arbitrarysymbols.

ts
interfaceColors {
[sym:symbol]:number;
}
 
constred =Symbol("red");
constgreen =Symbol("green");
constblue =Symbol("blue");
 
letcolors:Colors = {};
 
// Assignment of a number is allowed
colors[red] =255;
letredVal =colors[red];
let redVal: number
 
colors[blue] ="da ba dee";
Type 'string' is not assignable to type 'number'.2322Type 'string' is not assignable to type 'number'.
Try

Similarly, we can write an index signature with template string pattern type.One use of this might be to exempt properties starting withdata- from TypeScript’s excess property checking.When we pass an object literal to something with an expected type, TypeScript will look for excess properties that weren’t declared in the expected type.

ts
// @errors: 2322 2375
interfaceOptions {
width?:number;
height?:number;
}
leta:Options = {
width:100,
height:100,
"data-blah":true,
};
interfaceOptionsWithDataPropsextendsOptions {
// Permit any property starting with 'data-'.
[optName:`data-${string}`]:unknown;
}
letb:OptionsWithDataProps = {
width:100,
height:100,
"data-blah":true,
// Fails for a property which is not known, nor
// starts with 'data-'
"unknown-property":true,
};

A final note on index signatures is that they now permit union types, as long as they’re a union of infinite-domain primitive types - specifically:

  • string
  • number
  • symbol
  • template string patterns (e.g.`hello-${string}`)

An index signature whose argument is a union of these types will de-sugar into several different index signatures.

ts
interfaceData {
[optName:string |symbol]:any;
}
// Equivalent to
interfaceData {
[optName:string]:any;
[optName:symbol]:any;
}

For more details,read up on the pull request

Defaulting to theunknown Type in Catch Variables (--useUnknownInCatchVariables)

In JavaScript, any type of value can be thrown withthrow and caught in acatch clause.Because of this, TypeScript historically typed catch clause variables asany, and would not allow any other type annotation:

ts
try {
// Who knows what this might throw...
executeSomeThirdPartyCode();
}catch (err) {
// err: any
console.error(err.message);// Allowed, because 'any'
err.thisWillProbablyFail();// Allowed, because 'any' :(
}

Once TypeScript added theunknown type, it became clear thatunknown was a better choice thanany incatch clause variables for users who want the highest degree of correctness and type-safety, since it narrows better and forces us to test against arbitrary values.Eventually TypeScript 4.0 allowed users to specify an explicit type annotation ofunknown (orany) on eachcatch clause variable so that we could opt into stricter types on a case-by-case basis;however, for some, manually specifying: unknown on everycatch clause was a chore.

That’s why TypeScript 4.4 introduces a new flag calleduseUnknownInCatchVariables.This flag changes the default type ofcatch clause variables fromany tounknown.

ts
try {
executeSomeThirdPartyCode();
}catch (err) {
// err: unknown
 
// Error! Property 'message' does not exist on type 'unknown'.
console.error(err.message);
'err' is of type 'unknown'.18046'err' is of type 'unknown'.
 
// Works! We can narrow 'err' from 'unknown' to 'Error'.
if (errinstanceofError) {
console.error(err.message);
}
}
Try

This flag is enabled under thestrict family of options.That means that if you check your code usingstrict, this option will automatically be turned on.You may end up with errors in TypeScript 4.4 such as

Property 'message' does not exist on type 'unknown'.
Property 'name' does not exist on type 'unknown'.
Property 'stack' does not exist on type 'unknown'.

In cases where we don’t want to deal with anunknown variable in acatch clause, we can always add an explicit: any annotation so that we can optout of stricter types.

ts
try {
executeSomeThirdPartyCode();
}catch (err:any) {
console.error(err.message);// Works again!
}
Try

For more information, take a look atthe implementing pull request.

Exact Optional Property Types (--exactOptionalPropertyTypes)

In JavaScript, reading amissing property on an object produces the valueundefined.It’s also possible tohave an actual property with the valueundefined.A lot of code in JavaScript tends to treat these situations the same way, and so initially TypeScript just interpreted every optional property as if a user had writtenundefined in the type.For example,

ts
interfacePerson {
name:string;
age?:number;
}

was considered equivalent to

ts
interfacePerson {
name:string;
age?:number |undefined;
}

What this meant is that a user could explicitly writeundefined in place ofage.

ts
constp:Person = {
name:"Daniel",
age:undefined,// This is okay by default.
};

So by default, TypeScript doesn’t distinguish between a present property with the valueundefined and a missing property.While this works most of the time, not all code in JavaScript makes the same assumptions.Functions and operators likeObject.assign,Object.keys, object spread ({ ...obj }), andfor-in loops behave differently depending on whether or not a property actually exists on an object.In the case of ourPerson example, this could potentially lead to runtime errors if theage property was observed in a context where its presence was important.

In TypeScript 4.4, the new flagexactOptionalPropertyTypes specifies that optional property types should be interpreted exactly as written, meaning that| undefined is not added to the type:

ts
// With 'exactOptionalPropertyTypes' on:
constp:Person = {
Type '{ name: string; age: undefined; }' is not assignable to type 'Person' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties. Types of property 'age' are incompatible. Type 'undefined' is not assignable to type 'number'.2375Type '{ name: string; age: undefined; }' is not assignable to type 'Person' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties. Types of property 'age' are incompatible. Type 'undefined' is not assignable to type 'number'.
name:"Daniel",
age:undefined,// Error! undefined isn't a number
};
Try

This flag isnot part of thestrict family and needs to be turned on explicitly if you’d like this behavior.It also requiresstrictNullChecks to be enabled as well.We’ve been making updates to DefinitelyTyped and other definitions to try to make the transition as straightforward as possible, but you may encounter some friction with this depending on how your code is structured.

For more information, you cantake a look at the implementing pull request here.

static Blocks in Classes

TypeScript 4.4 brings support forstatic blocks in classes, an upcoming ECMAScript feature that can help you write more-complex initialization code for static members.

ts
classFoo {
staticcount =0;
 
// This is a static block:
static {
if (someCondition()) {
Foo.count++;
}
}
}
Try

These static blocks allow you to write a sequence of statements with their own scope that can access private fields within the containing class.That means that we can write initialization code with all the capabilities of writing statements, no leakage of variables, and full access to our class’s internals.

ts
classFoo {
static#count =0;
 
getcount() {
returnFoo.#count;
}
 
static {
try {
constlastInstances =loadLastInstances();
Foo.#count +=lastInstances.length;
}
catch {}
}
}
Try

Withoutstatic blocks, writing the code above was possible, but often involved several different types of hacks that had to compromise in some way.

Note that a class can have multiplestatic blocks, and they’re run in the same order in which they’re written.

ts
// Prints:
// 1
// 2
// 3
classFoo {
staticprop =1
static {
console.log(Foo.prop++);
}
static {
console.log(Foo.prop++);
}
static {
console.log(Foo.prop++);
}
}
Try

We’d like to extend our thanks toWenlu Wang for TypeScript’s implementation of this feature.For more details, you cansee that pull request here.

tsc --help Updates and Improvements

TypeScript’s--help option has gotten a refresh!Thanks to work in part bySong Gao, we’ve brought in changes toupdate the descriptions of our compiler options andrestyle the--help menu with colors and other visual separation.

The new TypeScript --help menu where the output is bucketed into several different areas

You can read more onthe original proposal thread.

Performance Improvements

Faster Declaration Emit

TypeScript now caches whether internal symbols are accessible in different contexts, along with how specific types should be printed.These changes can improve TypeScript’s general performance in code with fairly complex types, and is especially observed when emitting.d.ts files under thedeclaration flag.

See more details here.

Faster Path Normalization

TypeScript often has to do several types of “normalization” on file paths to get them into a consistent format that the compiler can use everywhere.This involves things like replacing backslashes with slashes, or removing intermediate/./ and/../ segments of paths.When TypeScript has to operate over millions of these paths, these operations end up being a bit slow.In TypeScript 4.4, paths first undergo quick checks to see whether they need any normalization in the first place.These improvements together reduce project load time by 5-10% on bigger projects, and significantly more in massive projects that we’ve tested internally.

For more details, you canview the PR for path segment normalization along withthe PR for slash normalization.

Faster Path Mapping

TypeScript now caches the way it constructs path-mappings (using thepaths option intsconfig.json).For projects with several hundred mappings, the reduction is significant.You can see moreon the change itself.

Faster Incremental Builds with--strict

In what was effectively a bug, TypeScript would end up redoing type-checking work underincremental compilations ifstrict was on.This led to many builds being just as slow as ifincremental was turned off.TypeScript 4.4 fixes this, though the change has also been back-ported to TypeScript 4.3.

See morehere.

Faster Source Map Generation for Big Outputs

TypeScript 4.4 adds an optimization for source map generation on extremely large output files.When building an older version of the TypeScript compiler, this results in around an 8% reduction in emit time.

We’d like to extend our thanks toDavid Michon who provided asimple and clean change to enable this performance win.

Faster--force Builds

When using--build mode on project references, TypeScript has to perform up-to-date checks to determine which files need to be rebuilt.When performing a--force build, however, that information is irrelevant since every project dependency will be rebuilt from scratch.In TypeScript 4.4,--force builds avoid those unnecessary steps and start a full build.See more about the changehere.

Spelling Suggestions for JavaScript

TypeScript powers the JavaScript editing experience in editors like Visual Studio and Visual Studio Code.Most of the time, TypeScript tries to stay out of the way in JavaScript files;however, TypeScript often has a lot of information to make confident suggestions, and ways of surfacing suggestions that aren’ttoo invasive.

That’s why TypeScript now issues spelling suggestions in plain JavaScript files - ones without// @ts-check or in a project withcheckJs turned off.These are the same“Did you mean…?” suggestions that TypeScript files already have, and now they’re available inall JavaScript files in some form.

These spelling suggestions can provide a subtle clue that your code is wrong.We managed to find a few bugs in existing code while testing this feature!

For more details on this new feature,take a look at the pull request!

Inlay Hints

TypeScript 4.4 provides support forinlay hints which can help display useful information like parameter names and return types in your code.You can think of it as a sort of friendly “ghost text”.

A preview of inlay hints in Visual Studio Code

This feature was built byWenlu Wang whosepull request has more details.

Wenlu also contributedthe integration for inlay hints in Visual Studio Code which has shipped aspart of the July 2021 (1.59) release.If you’d like to try inlay hints out, make sure you’re using a recentstable orinsiders version of the editor.You can also modify when and where inlay hints get displayed in Visual Studio Code’s settings.

Auto-Imports Show True Paths in Completion Lists

When editors like Visual Studio Code show a completion list, completions which include auto-imports are displayed with a path to the given module;however, this path usually isn’t what TypeScript ends up placing in a module specifier.The path is usually something relative to theworkspace, meaning that if you’re importing from a package likemoment, you’ll often see a path likenode_modules/moment.

A completion list containing unwieldy paths containing 'node_modules'. For example, the label for 'calendarFormat' is 'node_modules/moment/moment' instead of 'moment'.

These paths end up being unwieldy and often misleading, especially given that the path that actually gets inserted into your file needs to consider Node’snode_modules resolution, path mappings, symlinks, and re-exports.

That’s why with TypeScript 4.4, the completion item label now shows theactual module path that will be used for the import!

A completion list containing clean paths with no intermediate 'node_modules'. For example, the label for 'calendarFormat' is 'moment' instead of 'node_modules/moment/moment'.

Since this calculation can be expensive, completion lists containing many auto-imports may fill in the final module specifiers in batches as you type more characters. It’s possible that you’ll still sometimes see the old workspace-relative path labels; however, as your editing experience “warms up”, they should get replaced with the actual path after another keystroke or two.

Breaking Changes

lib.d.ts Changes for TypeScript 4.4

As with every TypeScript version, declarations forlib.d.ts (especially the declarations generated for web contexts), have changed.You can consultour list of knownlib.dom.d.ts changes to understand what is impacted.

More-Compliant Indirect Calls for Imported Functions

In earlier versions of TypeScript, calling an import from CommonJS, AMD, and other non-ES module systems would set thethis value of the called function.Specifically, in the following example, when callingfooModule.foo(), thefoo() method will havefooModule set as the value ofthis.

ts
// Imagine this is our imported module, and it has an export named 'foo'.
letfooModule = {
foo() {
console.log(this);
},
};
fooModule.foo();

This is not the way exported functions in ECMAScript are supposed to work when we call them.That’s why TypeScript 4.4 intentionally discards thethis value when calling imported functions, by using the following emit.

ts
// Imagine this is our imported module, and it has an export named 'foo'.
letfooModule = {
foo() {
console.log(this);
},
};
// Notice we're actually calling '(0, fooModule.foo)' now, which is subtly different.
(0,fooModule.foo)();

You canread up more about the changes here.

Usingunknown in Catch Variables

Users running with thestrict flag may see new errors aroundcatch variables beingunknown, especially if the existing code assumes onlyError values have been caught.This often results in error messages such as:

Property 'message' does not exist on type 'unknown'.
Property 'name' does not exist on type 'unknown'.
Property 'stack' does not exist on type 'unknown'.

To get around this, you can specifically add runtime checks to ensure that the thrown type matches your expected type.Otherwise, you can just use a type assertion, add an explicit: any to your catch variable, or turn offuseUnknownInCatchVariables.

Broader Always-Truthy Promise Checks

In prior versions, TypeScript introduced “Always Truthy Promise checks” to catch code where anawait may have been forgotten;however, the checks only applied to named declarations.That meant that while this code would correctly receive an error…

ts
asyncfunctionfoo():Promise<boolean> {
returnfalse;
}
asyncfunctionbar():Promise<string> {
constfooResult =foo();
if (fooResult) {
// <- error! :D
return"true";
}
return"false";
}

…the following code would not.

ts
asyncfunctionfoo():Promise<boolean> {
returnfalse;
}
asyncfunctionbar():Promise<string> {
if (foo()) {
// <- no error :(
return"true";
}
return"false";
}

TypeScript 4.4 now flags both.For more information,read up on the original change.

Abstract Properties Do Not Allow Initializers

The following code is now an error because abstract properties may not have initializers:

ts
abstractclassC {
abstractprop =1;
// ~~~~
// Property 'prop' cannot have an initializer because it is marked abstract.
}

Instead, you may only specify a type for the property:

ts
abstractclassC {
abstractprop:number;
}

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

Contributors to this page:
OTOrta Therox  (2)
ELEliran Levi  (1)
ABAndrew Branch  (1)
JBJack Bates  (1)

Last updated: Dec 16, 2025