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:
At the time of this writing, version 3.8.3 is already out. So first, upgrade to the latest version:
npm install typescript@latest
or npm -g upgrade typescript
(add the -g
option if you’re using it globally)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.
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 declarationpreserve
, to keep the import declaration, useful to execute side-effectserror
, like preserve
but adds an error whenever an import
could be written as an import 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;
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 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
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:
target
compiler option must be es2017
or above,module
compiler option must be esnext
or system
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 filescheckJs
, which is used in conjunction with the option above and allows TypeScript to report errors in JavaScript filesTypeScript 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 subclassesFor 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.
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:
watchOptions
(more info here)For the editor (more information here):
And some breaking changes:
any
object
in JSDoc is no longer any
under noImplicitAny
You can find more information about these breaking changes here.
Happy coding!
LogRocket is a frontend application monitoring solution 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 and mobile apps.
Would you be interested in joining LogRocket's developer community?
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 nowExplore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
Build a real-time image background remover in Vue using Transformers.js and WebGPU for client-side processing with privacy and efficiency.
Optimize search parameter handling in React and Next.js with nuqs for SEO-friendly, shareable URLs and a better user experience.