Written by Esteban Herrera✏️
Typescript 3.8 was released on February 20th, 2020. This version includes changes to the compiler, performance, and editor.
In this post, I’m going to review five important changes to the compiler:
- Type-only imports and exports
- export * as ns syntax
- ES2020 private fields
- Top-level await
- JSDoc property modifiers
At the time of this writing, version 3.8.3 is already out. So first, upgrade to the latest version:
- You can upgrade by using NPM, with commands like
npm install typescript@latest
ornpm -g upgrade typescript
(add the-g
option if you’re using it globally) - If you’re using Visual Studio, you can do so by downloading it here
- If you’re using Visual Studio Code, you can upgrade by modifying either the user settings or the workspace settings
- If you’re using Sublime Text, you can upgrade via PackageControl
In a terminal window, you can use the following command to confirm you’re using the latest version:
tsc --version
Now let’s start by reviewing the type-only import/export feature.
Type-only import and exports
This feature brings new syntax to import and export declarations, along with a new compiler option, importsNotUsedAsValues
:
import type { MyType } from "./my-module.js";
// ...
export type { MyType };
This gives you more control over how import statements are handled in the output, which is particularly useful when compiling using the --isolatedModules
option, the transpileModule
API, or Babel.
By default, TypeScript drops import statements when the imports are only used as types. For example, consider the following:
// lion.ts
export default class Lion {}
// zoo.ts
import Lion from './lion';
let myLion: Lion;
This will be the output when you compile zoo.ts
:
// zoo.js
let myLion: Lion;
Usually, this won’t be a problem. But what if there’s a side-effect in the Lion
module:
// lion.ts
export default class Lion {}
console.log("Here's an important message about lions: ... ");
In this case, if the import statement is dropped from the output, the console.log
statement will never be executed.
The import statement will also be dropped if you declare it like this:
import {TypeA, Type2} from "./my-module";
However, it will be kept in the output if you declare it like this:
import "./my-module";
A bit confusing, right?
Here’s another problem. In the following code, can you tell if X
is a value or a type?
import { X } from "./my-module.js";
export { X };
Knowing this might be important, Babel and TypeScript’s transpileModule
API will output code that doesn’t work correctly if X
is only a type, and TypeScript’s isolatedModules
flag will generate a warning.
On the other hand, we can have a similar problem with exports, where a re-export of a type should be omitted but the compiler can’t tell that we’re just re-exporting a type during single-file Babel transpilation, for example.
In TypeScript 3.8, import type
and export type
make explicit the importing/exporting of types.
These are some valid ways of using them:
import type MyType from './my-module';
import type { MyTypeA, MyTypeB } from './my-module';
import type * as Types from './my-module';
export type { MyType };
export type { MyType } from './module';
And here are some invalid ways:
import { type MyType } from './my-module';
import type MyType, { FunctionA } from './my-module';
export { FunctionA, type MyType } from './my-module';
Keep in mind that if the type is not used as a type, the compiler will mark this as an error:
import type Lion from './lion';
let myLion: Lion; // Valid
myLion = new Lion(); // Invalid: 'Lion' cannot be used as a value because it was imported using 'import type'.
When using import type
, the behavior is to drop the import declaration from the JavaScript file, as usual. But in TypeScript 3.8, when using import
, the behavior can be controlled with the compiler option importsNotUsedAsValue
, which can take the values:
-
default
, to omit the import declaration -
preserve
, to keep the import declaration, useful to execute side-effects -
error
, likepreserve
but adds an error whenever animport
could be written as animport type
This way, by adding the option "importsNotUsedAsValue":"preserve"
to the tsconfig.json
file:
// tsconfig.json
{
"compilerOptions": {
// ...
"importsNotUsedAsValues": "preserve"
},
// ...
}
This TypeScript code:
import Lion from './lion';
let myLion: Lion;
Compiles to this JavaScript code:
import './Lion';
let myLion;
Export * as ns syntax
TypeScript supports some of the newer ECMAScript 2020 features, such as export * as namespace
declarations.
Sometimes, it’s useful to have something like this:
import * as animals from "./animals.js";
export { animals };
Which exposes all the members of another module as a single member.
In ES2020, this can be expressed as one statement:
export * as animals from "./animals";
TypeScript 3.8 supports this syntax.
If you configure the module for ES2020:
// tsconfig.json
{
"compilerOptions": {
"module": "ES2020",
// ...
}
}
TypeScript will output the statement without modifications:
// allAnimals.ts
export * as animals from "./animals";
// allAnimals.js
export * as animals from "./animals";
But if you configure the module with something earlier, for example:
// tsconfig.json
{
"compilerOptions": {
"module": "ES2015",
// ...
}
}
TypeScript will output these two declarations:
import * as animals_1 from "./animals";
export { animals_1 as animals };
ES2020 private fields
ES2020 also brings a new syntax for private fields. Here’s an example:
class Lion {
#age: number;
constructor(age: number) {
this.#age = age;
}
getAge() {
return this.#age;
}
}
Private fields start with the #
character, and just like fields marked with the private
keyword, they are scoped to their containing class.
Why the #
character? Well, apparently all the other cool characters were already taken or could lead to invalid code.
However, there are some rules.
First of all, you cannot use the private
modifier and the #
character on the same field at the same time (or the public
modifier, although the combination wouldn’t make sense anyway).
Does this mean that the private
modifier is going to disappear eventually?
At the time of this writing, there’s an open discussion about this, but the current plan is to leave it as it is.
So, which one should you use?
Well, it depends on how strict you want to be about privacy.
The thing with the private
modifier is that it’s only recognized by TypeScript, which means that the access restriction is only enforced at compile-time and the private constraint will get erased from the generated JavaScript code, where the private field can be accessed without problems.
On the other hand, the #
character will be preserved in the JavaScript code, making the field completely inaccessible outside of the class.
Another rule when using #
is that private fields always have to be declared before they’re used:
class Lion {
constructor(age: number) {
// Error: Property '#age' does not exist on type 'Lion'.ts
this.#age = age;
}
}
Also, notice how you have to reference the private field with this
, otherwise, an error will be marked:
class Lion {
#age: number;
// ...
getAge() {
// The following line throws two errors:
// 1. Private identifiers are not allowed outside class bodies.
// 2. Cannot find name '#age'
return #age;
}
}
In order to use fields marked with #
, you must target ECMAScript 2015 (ES6) or higher:
// tsconfig.json
{
"compilerOptions": {
"target": "ES6",
// ...
}
}
The reason is that the implementation to enforce privacy uses WeakMap
s, which can’t be polyfilled in a way that doesn’t cause memory leaks, not all runtimes optimize the use of WeakMap
s. In contrast, fields with the private
modifier work with all targets and are faster.
For example, this TypeScript class:
// lion.ts
class Lion {
#age: number;
constructor(age: number) {
this.#age = age;
}
getAge() {
return this.#age;
}
}
Outputs this:
// lion.js
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, privateMap, value) {
if (!privateMap.has(receiver)) {
throw new TypeError("attempted to set private field on non-instance");
}
privateMap.set(receiver, value);
return value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, privateMap) {
if (!privateMap.has(receiver)) {
throw new TypeError("attempted to get private field on non-instance");
}
return privateMap.get(receiver);
};
var _age;
class Lion {
constructor(age) {
_age.set(this, void 0);
__classPrivateFieldSet(this, _age, age);
}
getAge() {
return __classPrivateFieldGet(this, _age);
}
}
_age = new WeakMap();
And from the output of the following code, you can see the age
property cannot be seen directly outside its class:
const lion = new Lion(5);
console.log(lion); // 'Lion {}'
console.log(Object.keys(lion)); // '[]'
console.log(lion.getAge()); // 5
Top-level await
In JavaScript, we can use async
functions with await expressions to perform asynchronous operations:
async function getLionInformation() {
let response = await fetch('/animals/lion.json');
let lion = await response.json();
return lion;
}
getLionInformation().then((value) => console.log(value));
But await
expressions are only allowed in the body of async
functions, we cannot use them in top-level code (which can be useful when using the developer console on Chrome, for example):
// Syntax error
let response = await fetch('/animals/lion.json');
let lion = await response.json();
console.log(lion);
However, top-level await (at this time a Stage 3 proposal for ECMAScript) allows us to use await
directly at the top level of a module or a script.
TypeScript 3.8 supports top-level await, and since files with import
and export
expressions are considered modules, even a simple export {}
would be enough to make this syntax work:
let response = await fetch('/animals/lion.json');
let lion = await response.json();
console.log(lion);
export {}
The only restrictions in TypeScript are that:
- The
target
compiler option must bees2017
or above, - The
module
compiler option must beesnext
orsystem
JSDoc property modifiers
JSDoc allows us to add documentation comments directly to JavaScript source code so the JSDoc tool can scan the code and generate an HTML documentation website.
But more than for documentation purposes, TypeScript uses JSDoc for type-checking JavaScript files.
This is possible due to two compiler options:
-
allowJs
, which allows TypeScript to compile JavaScript files -
checkJs
, which is used in conjunction with the option above and allows TypeScript to report errors in JavaScript files
TypeScript 3.8 adds support for three accessibility modifiers:
-
@public
, which means that the property can be used from anywhere (the default behavior) -
@private
, which means that a property can only be used within the class that defines it -
@protected
, which means that a property can only be used within the class that defines it and all the derived subclasses
For example:
// lion.js
// @ts-check
class Lion {
constructor() {
/** @private */
this.age = 5;
}
}
// Error: Property 'age' is private and only accessible within class 'Lion'.
console.log(new Lion().age);
And the @readonly
modifier, which ensures that a property is only ever assigned a value during initialization:
// lion.js
// @ts-check
class Lion {
constructor(ageParam) {
/** @readonly */
this.age = ageParam;
}
setAge(ageParam) {
// Error: Cannot assign to 'age' because it is a read-only property
this.age = ageParam;
}
}
You can learn more about type checking JavaScript files here, along with the supported JSDoc tags.
Conclusion
In this post, you have learned about five new features in TypeScript 3.8, type-only imports and exports, the export * as ns
syntax, ES2020 private fields, top-level await, and JSDoc property modifiers.
Of course, there are more new features.
For the compiler :
- Better directory watching on Linux and
watchOptions
(more info here) - “Fast and Loose” incremental checking (more info here)
For the editor (more information here):
- Refactor string concatenations (see this and this other issue)
- Show call hierarchies (see this issue and this pull request)
And some breaking changes :
- Stricter assignability checks to unions with index signatures
- Optional arguments with no inferences are correctly marked as implicitly
any
-
object
in JSDoc is no longerany
undernoImplicitAny
You can find more information about these breaking changes here.
Happy coding!
Plug: LogRocket, a DVR for web apps
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
Try it for free.
The post What’s new in TypeScript 3.8 appeared first on LogRocket Blog.
Top comments (0)