DEV Community

Cover image for What’s new in TypeScript 4.7
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

What’s new in TypeScript 4.7

Written by Ishan Manandhar✏️

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.

Setting up the environment

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
Enter fullscreen mode Exit fullscreen mode

We'll install the latest version of TypeScript with the -D flag, installing it as a dev dependency:

npm install typescript@rc -D
Enter fullscreen mode Exit fullscreen mode

Next, we’ll run the --init command to initialize TypeScript:

npx tsc --init
Enter fullscreen mode Exit fullscreen mode

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. */,

 }
}
Enter fullscreen mode Exit fullscreen mode

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"
},
Enter fullscreen mode Exit fullscreen mode

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.

Better control over module detection

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.

Objects and methods have improved in function inference

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(),
});
Enter fullscreen mode Exit fullscreen mode

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.

Specialized generic function with instantiation expressions

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 };
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>;
Enter fullscreen mode Exit fullscreen mode

We can also just receive a specific type and reject anything else:

const makeAppleJuice = makeJuice<number>;

// TypeScript correctly rejects this.
makeAppleJuice('Apple');
Enter fullscreen mode Exit fullscreen mode

This allows developers to specialize in the generic function and accept and reject good values.

More control flow analysis for calculated properties

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}`);
}
Enter fullscreen mode Exit fullscreen mode

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.

Object method snippet autocompletion inside IDE

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.

Object Method Snippet Autocompletion Inside IDE

typeof queries in #private fields are now allowed

With 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;
 }
}
Enter fullscreen mode Exit fullscreen mode

Optional variance annotations for type parameters

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

We use out and in modifiers here because a type parameter’s variance relies on whether it’s used in output or input.

Group-aware organize imports with auto sorting

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";
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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() types

TypeScript 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 {}
Enter fullscreen mode Exit fullscreen mode

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 {}
Enter fullscreen mode Exit fullscreen mode

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'.
Enter fullscreen mode Exit fullscreen mode

Customization of the resolution with moduleSuffixes

TypeScript 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", ""]
 }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 variables

Constraints 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]>;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.

ECMAScript module is supported in Node.js

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",
 }
}
Enter fullscreen mode Exit fullscreen mode

Other breaking changes

There are a few other exciting breaking changes with this update. Let’s discuss a few of them below!

Stricter spread checks in JSX

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} />;
}
Enter fullscreen mode Exit fullscreen mode

With the code above, we will receive an error like this:

Spread types may only be created from object types.
Enter fullscreen mode Exit fullscreen mode

Stricter checks with template string expression

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];
}
Enter fullscreen mode Exit fullscreen mode

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(...)'.
Enter fullscreen mode Exit fullscreen mode

readonly tuples have a read-only length property

The 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;
}
Enter fullscreen mode Exit fullscreen mode

readFile method is no longer optional on LanguageServiceHost

If 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.

Conclusion

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.


Writing a lot of TypeScript? Watch the recording of our recent TypeScript meetup to learn about writing more readable code.

Write More Readable Code with TypeScript 4.4

TypeScript brings type safety to JavaScript. There can be a tension between type safety and readable code. Watch the recording for a deep dive on some new features of TypeScript 4.4.

Top comments (1)

Collapse
 
andrewbaisden profile image
Andrew Baisden

Good walkthrough thanks for the news update.