There have been a lot of significant updates, introductions, and improvements included in TypeScript 4.7. This update specifically revolves around new features for type inference, narrowing analysis, ES module integration, instantiation expressions, and more.
In this article, we’ll take a look at each of the new changes and understand how we can use them today. Let’s go through each of them and understand what the new release implementation looks like.
typeof queries in #private fields are now allowedResolution mode can be used on import() typesmoduleSuffixesExtends constraints on infer type variablesThe Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
To start with the implementation of features, we’ll need to tune in to our local dev environment and configure some files.
First, run the command below to see a package.json file created for us in the root project directory:
mkdir ts4.7_walkthrough cd ts4.7_walkthrough npm init -y
We’ll install the latest version of TypeScript with the -D flag, installing it as a dev dependency:
npm install typescript@rc -D
Next, we’ll run the --init command to initialize TypeScript:
npx tsc --init
This will create a tsconfig.json file for us. We will add an option here so we can specify the output folder of our files:
{
"compilerOptions": {
"outDir": "./output"
"declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,
}
}
Until now, we have set the output directory to get the compiled JavaScript that’ll go into the folder. We’ll also update the scripts section of our package.json file to include the build and start script:
<
"scripts": {
"build": "tsc",
"start": "tsc -w"
},
With this in place, we can run our application with the command npm start, which listens to all changes to TypeScript files and compiles them down for us.
Now, let’s get our hands dirty exploring the newly added features and improvements.
Until this release, we needed to add type=module or .mjs extensions when writing our server-side code in Node. But, in plain JavaScript, modularized code runs slightly differently than traditional script code. Since they have different scoping rules, we need to decide how each file runs.
In TypeScript, these files are treated as modules if any imports and exports are written in the file. With this latest release, we have an option called moduleDetection to give us more control. This option can take three values: auto, legacy, and force. Let’s understand what each of them does.
When using auto mode, TypeScript will check for import and export statements, as well as whether the current file is in JSX when running under --jsx react-jsx. It’ll also check if the type field in package.json is set to the module when running under --module nodenext/--module node12.
legacy mode, on the other hand, only checks for import and export statements.
Finally, force mode will force every file to be treated as a module.
With the new version of TypeScript, we can perform more refined inferences from functions within objects, methods, and arrays.
We tend to infer types from context-insensitive function arguments anywhere in the argument list, such as when we are contextually typing parameters of arrow functions, object literals, and function expressions in a generic function argument list, or from context-sensitive function arguments in preceding positions in the argument list.
// Improved function inference in objects and methods
declare function myCharacter<T>(arg: {
characterLetter: (a: string) => T,
changeCharacter: (x: T) => void
}
): void;
// Works fine
myCharacter({
characterLetter: () => "meow meow",
changeCharacter: x => x.slice(0, 4),
});
// Works fine
myCharacter({
characterLetter: (a: string) => a,
changeCharacter: x => x.substring(0, 4),
});
// Was an error, but now it works.
myCharacter({
characterLetter() { return "I love typescript" },
changeCharacter: x => x.toUpperCase(),
});
// Now works with ^@4.7
myCharacter({
characterLetter: a => a,
changeCharacter: x => x.toUpperCase(),
});
// Now works with ^@4.7
myCharacter({
characterLetter: function () { return "I love typescript"; },
changeCharacter: x => x.toUpperCase(),
});
With these changes, we can now have the same left-to-right rules for information flow between context-sensitive, contextually-typed functions, regardless of whether the functions occur as discrete arguments or properties in the object or array literals.
We can now specialize the generic function with instantiation expressions. To demonstrate this,, we’ll create a generic type interface called makeJuice. It will take in a generic to be passed into a general function:
interface Fruits<T> {
value: T;
}
function makeJuice<T>(value: T) {
return { value };
}
There are often cases when we want to create a specialized function and wrap the function to make it more specialized. To achieve this, we can write:
function orangeJuice(fruit: Orange) {
return makeJuice(Orange);
}
// or can be written like
const appleJuice: (fruit: Fruits) => Fruits<Apple> = makeJuice;
This method definitely works, but creating a new function to wrap another function is a waste of time and, frankly, too much work. With the new release, we can simplify this by taking functions and constructors and feeding them type arguments directly:
const appleJuice= makeJuice<Apple>; const orangeJuice= makeJuice<Orange>;
We can also just receive a specific type and reject anything else:
const makeAppleJuice = makeJuice<number>;
// TypeScript correctly rejects this.
makeAppleJuice('Apple');
This allows developers to specialize in the generic function and accept and reject good values.
With the release of TypeScript 4.7, the TypeScript compiler can now parse the type of computed properties and reduce them correctly. You can see an example of this below:
const key = Symbol();
const objectKey = Math.random() <= 0.2 ? 90 : "Lucky person!";
let obj = {
[key]: objectKey,
};
if (typeof obj[key] === "string") {
let str = obj[key].toUpperCase();
console.log(`You really are ${str}`);
}
The new version knows that obj[key] is a string. TypeScript can correctly check that computed properties are initialized by the end of a constructor body.
We can now receive snippet completions for object literal methods. TypeScript will provide us with a typical completion entry for the name of the method only, as well as an entry for separate completion for the full method definition.

typeof queries in #private fields are now allowedWith this update, we are now allowed to perform typeof queries on private fields. You can see an example of this below:
class Programming {
#str = "Typescript rocks!";
get ourString(): typeof this.#str {
return this.#str;
}
set ourString(value: typeof this.#str) {
this.#str = value;
}
}
Further, we are now able to explicitly specify variance on type parameters:
interface Programming {
langList: string[];
}
interface typescriptLang extends Programming {
tsLang: string;
}
type Getter<T> = (value: T) => T;
type Setter<T> = (value: T) => void;
Let’s assume we have two different Getters that are substitutable, entirely depending on generic T. In such a case, we can check that Getter<TypeScript> → Getter<Programming> is valid. In other words, we need to check if TypeScript → Programming is valid.
Checking if Setter<Typescript> → Setter<Programming> is valid involves seeing whether Typescript → Programming is also valid. That flip in direction is similar to logic in math. Essentially, we are seeing whether −x < −y is the same y < x.
When we need to flip directions like this to compare T, we say that Setter is contravariant on T.
We can now explicitly state that Getter is covariant on T with the help of the out modifier:
type Getter<out T> = () => T;
Similarly, if we want to make it explicit that the Setter is contravariant on T, we can give it a modifier:
type Setter<in T> = (value: T) => void;
We use out and in modifiers here because a type parameter’s variance relies on whether it’s used in output or input.
TypeScript now has an organized imports editor for both JavaScript and TypeScript, however, it may not meet our expectations. It’s actually better to natively sort our import statements.
Let’s take an example in action:
// local code import * as cost from "./cost"; import * as expenses from "./expenses"; import * as budget from "./budget"; // built-ins import * as fs from "fs"; import * as http from "http" import * as path from "path"; import * as crypto from "crypto";
If we run organize imports on the following file, we’d see the following changes:
// built-ins import * as fs from "fs"; import * as http from "http" import * as path from "path"; import * as crypto from "crypto"; //local code import * as cost from "./cost"; import * as expenses from "./expenses"; import * as budget from "./budget";
The imports are sorted by their paths and our comments and newlines are preserved. This organizes imports in a group-aware manner.
Resolution mode can be used on import() typesTypeScript now allows /// <reference types="…" /> directives and import type statements to specify a resolution strategy. This means we can resolve the imported Node ECMAScript resolution. However, it would be useful to reference the types of common JavaScript modules from an ECMAScript module or vice versa.
In nightly versions of TypeScript, the import type can specify an import assertion to achieve similar results:
// Resolve `pkg` as if we were importing with a `require()`
import type { TypeFromRequire } from "pkg" assert {
"resolution-mode": "require"
};
// Resolve `pkg` as if we were importing with an `import`
import type { TypeFromImport } from "pkg" assert {
"resolution-mode": "import"
};
export interface MergedType extends TypeFromRequire, TypeFromImport {}
These import assertions can also be used on import() types as well:
export type TypeFromRequire =
import("pkg", { assert: { "resolution-mode": "require" } }).TypeFromRequire;
export type TypeFromImport =
import("pkg", { assert: { "resolution-mode": "import" } }).TypeFromImport;
export interface MergedType extends TypeFromRequire, TypeFromImport {}
Note that the import type and import() syntax only supports resolution mode in nightly builds of TypeScript. We would get errors such as:
**Resolution mode assertions are unstable. Use nightly TypeScript to silence this error. Try updating with 'npm install -D typescript@next'.
moduleSuffixesTypeScript now supports a moduleSuffixes option to customize how module specifiers are looked up. For example, if we are importing files like import * as foo from "./foo" and moduleSuffixes configurations, it looks something like this:
{
"compilerOptions": {
"moduleSuffixes": [".ios", ".native", ""]
}
}
With this configuration, we are forcing our application to look into the relative files in the path:
./example.ios.ts ./example.native.ts ./example.ts
This feature will become really handy, especially in React Native projects where we add each targeted platform with different moduleSuffixes inside of tsconfig.json.
Note that the empty string "" in moduleSuffixes is necessary for TypeScript to also lookup ./example.ts.
Extends constraints on infer type variablesConstraints on infer type variables allow developers to match and infer against the shape of types, as well as make decisions based upon them:
type FirstStringCheck<T> =
T extends [infer S, ...unknown[]]
? S extends string ? S : never
: never;
// string
type A = FirstStringCheck<[string, number, number]>;
// "typescript"
type B = FirstStringCheck<["typescript", number, number]>;
// "typescript" | "rocks"
type C = FirstStringCheck<["typescript" | "rocks", boolean]>;
// never
type D = FirstStringCheck<[boolean, number, string]>;
FirstStringCheck matches against any tuple type with at least one element and grabs the first element’s type as S. It will then check if S is compatible with the string and return the type if it is.
Previously, we needed to write the same logic of FirstStringCheck like this:
type FirstStringCheck<T> =
T extends [string, ...unknown[]]
// Grab the first type out of `T`
? T[0]
: never;
We are becoming more manual and less declarative in this case. Rather than just sticking with pattern matching on the type definition, we are providing a name to the first element and extracting the [0] element of T.
This new version allows us to place a constraint on any inferred type, as seen in the example below:
type FirstStringCheck<T> = T extends [infer S extends string, ...unknown[]] ? S : never;
When S matches, it will make sure that S is a type of string. If it’s not, it will take a false path of never.
The support for ECMAScript has been a tough task for Node.js since its ecosystem is built around CommonJS. Node.js extended its support for the ECMAScript module in their v12 update.
TypeScript v4.5 has rolled out this support for ESM in Node.js as a nightly feature to get feedback from the users. Now, it has introduced two new compiler options, node12 and nodenext, to extend ECMAScript module support. With the arrival of these two features, it enables several other exciting features for us.
{
"compilerOptions": {
"module": "nodenext",
}
}
There are a few other exciting breaking changes with this update. Let’s discuss a few of them below!
While writing a …spread inside of JSX, we have a more strictly enforced rule inbuilt into the library. The values of unknown and never (as well as null and undefined) can no longer be spread into JSX elements. This makes the behavior more consistent with spreads in object literals.
import React from "react";
interface Props {
id?: string;
}
function Homepage(props: unknown) {
return <div {...props} />;
}
With the code above, we will receive an error like this:
Spread types may only be created from object types.
If we use a symbol value in JavaScript, both JavaScript and TypeScript will throw an error. However, TypeScript now checks if a generic value contained in a symbol is used in the template string.
function keyPropertyLogger<S extends string | symbol>(key: S): S {
// Now an error.
console.log(`${key} is the key`);
return key;
}
function get<T, K extends keyof T>(obj: T, key: K) {
// Now an error.
console.log(`This is a key ${key}`);
return obj[key];
}
TypeScript will now throw us an error with the following issue:
Implicit conversion of a 'symbol' to a 'string' will fail at runtime. Consider wrapping this expression in 'String(...)'.
readonly tuples have a read-only length propertyThe readonly tuple will now treat the length property as read-only. This can be seen for tuples with optional trailing and rest element types.
function strLength(tuple: readonly [string, string, string]) {
// Throws an error now
tuple.length = 7;
}
readFile method is no longer optional on LanguageServiceHostIf we are creating a LanguageService instance, LanguageServiceHost will need to provide a readFile method. This was a needed change in order to support the new module detection compiler option.
With a lot of effort and hard work from the team and contributors, we can now try the exciting new features and improvements with TypeScript 4.7.
There are a lot of handy features we can use to scale and optimize our time and efficiency. A comprehensive list of release topics can be found through the Microsoft blog, an excellent resource to dive into.
LogRocket lets you replay user sessions, eliminating guesswork by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings — compatible with all frameworks, and with plugins to log additional context from Redux, Vuex, and @ngrx/store.
With Galileo AI, you can instantly identify and explain user struggles with automated monitoring of your entire product experience.
Modernize how you understand your web and mobile apps — start monitoring for free.

line-clamp to trim lines of textMaster the CSS line-clamp property. Learn how to truncate text lines, ensure cross-browser compatibility, and avoid hidden UX pitfalls when designing modern web layouts.

Discover seven custom React Hooks that will simplify your web development process and make you a faster, better, more efficient developer.

Promise.all still relevant in 2025?In 2025, async JavaScript looks very different. With tools like Promise.any, Promise.allSettled, and Array.fromAsync, many developers wonder if Promise.all is still worth it. The short answer is yes — but only if you know when and why to use it.

Discover what’s new in The Replay, LogRocket’s newsletter for dev and engineering leaders, in the October 29th issue.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now