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
- Better control over module detection
- Objects and methods have improved in function inference
- Specialized generic function with instantiation expressions
- More control flow analysis for calculated properties
- Object method snippet autocompletion inside IDE
-
typeof
queries in#private
fields are now allowed - Optional variance annotations for type parameters
- Group-aware organize imports with auto sorting
-
Resolution
mode can be used onimport()
types - Customization of the resolution with
moduleSuffixes
-
Extends
constraints oninfer
type variables - ECMAScript module is supported in Node.js
- Other breaking changes
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
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.
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(),
});
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 };
}
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.
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}`);
}
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.
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;
}
}
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;
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.
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";
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()
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 {}
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'.
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", ""]
}
}
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 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]>;
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
.
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",
}
}
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} />;
}
With the code above, we will receive an error like this:
Spread types may only be created from object types.
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];
}
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 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;
}
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.
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)
Good walkthrough thanks for the news update.