TypeScript 3.8
Type-Only Imports and Export
This feature is something most users may never have to think about; however, if you’ve hit issues underisolatedModules, TypeScript’stranspileModule API, or Babel, this feature might be relevant.
TypeScript 3.8 adds a new syntax for type-only imports and exports.
tsimporttype {SomeThing }from"./some-module.js";exporttype {SomeThing };
import type only imports declarations to be used for type annotations and declarations.Italways gets fully erased, so there’s no remnant of it at runtime.Similarly,export type only provides an export that can be used for type contexts, and is also erased from TypeScript’s output.
It’s important to note that classes have a value at runtime and a type at design-time, and the use is context-sensitive.When usingimport type to import a class, you can’t do things like extend from it.
tsimporttype {Component }from"react";interfaceButtonProps {// ...}classButtonextendsComponent<ButtonProps> {// ~~~~~~~~~// error! 'Component' only refers to a type, but is being used as a value here.// ...}
If you’ve used Flow before, the syntax is fairly similar.One difference is that we’ve added a few restrictions to avoid code that might appear ambiguous.
ts// Is only 'Foo' a type? Or every declaration in the import?// We just give an error because it's not clear.importtypeFoo, {Bar,Baz }from"some-module";// ~~~~~~~~~~~~~~~~~~~~~~// error! A type-only import can specify a default import or named bindings, but not both.
In conjunction withimport type, TypeScript 3.8 also adds a new compiler flag to control what happens with imports that won’t be utilized at runtime:importsNotUsedAsValues.This flag takes 3 different values:
remove: this is today’s behavior of dropping these imports. It’s going to continue to be the default, and is a non-breaking change.preserve: thispreserves all imports whose values are never used. This can cause imports/side-effects to be preserved.error: this preserves all imports (the same as thepreserveoption), but will error when a value import is only used as a type. This might be useful if you want to ensure no values are being accidentally imported, but still make side-effect imports explicit.
For more information about the feature, you cantake a look at the pull request, andrelevant changes around broadening where imports from animport type declaration can be used.
ECMAScript Private Fields
TypeScript 3.8 brings support for ECMAScript’s private fields, part of thestage-3 class fields proposal.
tsclassPerson {#name:string;constructor(name:string) {this.#name =name;}greet() {console.log(`Hello, my name is${this.#name}!`);}}letjeremy =newPerson("Jeremy Bearimy");jeremy.#name;// ~~~~~// Property '#name' is not accessible outside class 'Person'// because it has a private identifier.
Unlike regular properties (even ones declared with theprivate modifier), private fields have a few rules to keep in mind.Some of them are:
- Private fields start with a
#character. Sometimes we call theseprivate names. - Every private field name is uniquely scoped to its containing class.
- TypeScript accessibility modifiers like
publicorprivatecan’t be used on private fields. - Private fields can’t be accessed or even detected outside of the containing class - even by JS users! Sometimes we call thishard privacy.
Apart from “hard” privacy, another benefit of private fields is that uniqueness we just mentioned.For example, regular property declarations are prone to being overwritten in subclasses.
tsclassC {foo =10;cHelper() {returnthis.foo;}}classDextendsC {foo =20;dHelper() {returnthis.foo;}}letinstance =newD();// 'this.foo' refers to the same property on each instance.console.log(instance.cHelper());// prints '20'console.log(instance.dHelper());// prints '20'
With private fields, you’ll never have to worry about this, since each field name is unique to the containing class.
tsclassC {#foo =10;cHelper() {returnthis.#foo;}}classDextendsC {#foo =20;dHelper() {returnthis.#foo;}}letinstance =newD();// 'this.#foo' refers to a different field within each class.console.log(instance.cHelper());// prints '10'console.log(instance.dHelper());// prints '20'
Another thing worth noting is that accessing a private field on any other type will result in aTypeError!
tsclassSquare {#sideLength:number;constructor(sideLength:number) {this.#sideLength =sideLength;}equals(other:any) {returnthis.#sideLength ===other.#sideLength;}}consta =newSquare(100);constb = {sideLength:100 };// Boom!// TypeError: attempted to get private field on non-instance// This fails because 'b' is not an instance of 'Square'.console.log(a.equals(b));
Finally, for any plain.js file users, private fieldsalways have to be declared before they’re assigned to.
jsclassC {// No declaration for '#foo'// :(constructor(foo:number) {// SyntaxError!// '#foo' needs to be declared before writing to it.this.#foo =foo;}}
JavaScript has always allowed users to access undeclared properties, whereas TypeScript has always required declarations for class properties.With private fields, declarations are always needed regardless of whether we’re working in.js or.ts files.
jsclassC {/**@type{number} */#foo;constructor(foo:number) {// This works.this.#foo =foo;}}
For more information about the implementation, you cancheck out the original pull request
Which should I use?
We’ve already received many questions on which type of privates you should use as a TypeScript user: most commonly, “should I use theprivate keyword, or ECMAScript’s hash/pound (#) private fields?”It depends!
When it comes to properties, TypeScript’sprivate modifiers are fully erased - that means that at runtime, it acts entirely like a normal property and there’s no way to tell that it was declared with aprivate modifier. When using theprivate keyword, privacy is only enforced at compile-time/design-time, and for JavaScript consumers it’s entirely intent-based.
tsclassC {privatefoo =10;}// This is an error at compile time,// but when TypeScript outputs .js files,// it'll run fine and print '10'.console.log(newC().foo);// prints '10'// ~~~// error! Property 'foo' is private and only accessible within class 'C'.// TypeScript allows this at compile-time// as a "work-around" to avoid the error.console.log(newC()["foo"]);// prints '10'
The upside is that this sort of “soft privacy” can help your consumers temporarily work around not having access to some API, and also works in any runtime.
On the other hand, ECMAScript’s# privates are completely inaccessible outside of the class.
tsclassC {#foo =10;}console.log(newC().#foo);// SyntaxError// ~~~~// TypeScript reports an error *and*// this won't work at runtime!console.log(newC()["#foo"]);// prints undefined// ~~~~~~~~~~~~~~~// TypeScript reports an error under 'noImplicitAny',// and this prints 'undefined'.
This hard privacy is really useful for strictly ensuring that nobody can take use of any of your internals.If you’re a library author, removing or renaming a private field should never cause a breaking change.
As we mentioned, another benefit is that subclassing can be easier with ECMAScript’s# privates because theyreally are private.When using ECMAScript# private fields, no subclass ever has to worry about collisions in field naming.When it comes to TypeScript’sprivate property declarations, users still have to be careful not to trample over properties declared in superclasses.
One more thing to think about is where you intend for your code to run.TypeScript currently can’t support this feature unless targeting ECMAScript 2015 (ES6) targets or higher.This is because our downleveled implementation usesWeakMaps to enforce privacy, andWeakMaps can’t be polyfilled in a way that doesn’t cause memory leaks.In contrast, TypeScript’sprivate-declared properties work with all targets - even ECMAScript 3!
A final consideration might be speed:private properties are no different from any other property, so accessing them is as fast as any other property access no matter which runtime you target.In contrast, because# private fields are downleveled usingWeakMaps, they may be slower to use.While some runtimes might optimize their actual implementations of# private fields, and even have speedyWeakMap implementations, that might not be the case in all runtimes.
export * as ns Syntax
It’s often common to have a single entry-point that exposes all the members of another module as a single member.
tsimport*asutilitiesfrom"./utilities.js";export {utilities };
This is so common that ECMAScript 2020 recently added a new syntax to support this pattern!
tsexport*asutilitiesfrom"./utilities.js";
This is a nice quality-of-life improvement to JavaScript, and TypeScript 3.8 implements this syntax.When your module target is earlier thanes2020, TypeScript will output something along the lines of the first code snippet.
Top-Levelawait
TypeScript 3.8 provides support for a handy upcoming ECMAScript feature called “top-levelawait“.
JavaScript users often introduce anasync function in order to useawait, and then immediately called the function after defining it.
jsasyncfunctionmain() {constresponse =awaitfetch("...");constgreeting =awaitresponse.text();console.log(greeting);}main().catch((e)=>console.error(e));
This is because previously in JavaScript (along with most other languages with a similar feature),await was only allowed within the body of anasync function.However, with top-levelawait, we can useawait at the top level of a module.
tsconstresponse =awaitfetch("...");constgreeting =awaitresponse.text();console.log(greeting);// Make sure we're a moduleexport {};
Note there’s a subtlety: top-levelawait only works at the top level of amodule, and files are only considered modules when TypeScript finds animport or anexport.In some basic cases, you might need to write outexport {} as some boilerplate to make sure of this.
Top levelawait may not work in all environments where you might expect at this point.Currently, you can only use top levelawait when thetarget compiler option ises2017 or above, andmodule isesnext orsystem.Support within several environments and bundlers may be limited or may require enabling experimental support.
For more information on our implementation, you cancheck out the original pull request.
es2020 fortarget andmodule
TypeScript 3.8 supportses2020 as an option formodule andtarget.This will preserve newer ECMAScript 2020 features like optional chaining, nullish coalescing,export * as ns, and dynamicimport(...) syntax.It also meansbigint literals now have a stabletarget belowesnext.
JSDoc Property Modifiers
TypeScript 3.8 supports JavaScript files by turning on theallowJs flag, and also supportstype-checking those JavaScript files via thecheckJs option or by adding a// @ts-check comment to the top of your.js files.
Because JavaScript files don’t have dedicated syntax for type-checking, TypeScript leverages JSDoc.TypeScript 3.8 understands a few new JSDoc tags for properties.
First are the accessibility modifiers:@public,@private, and@protected.These tags work exactly likepublic,private, andprotected respectively work in TypeScript.
js// @ts-checkclassFoo {constructor() {/**@private */this.stuff =100;}printStuff() {console.log(this.stuff);}}newFoo().stuff;// ~~~~~// error! Property 'stuff' is private and only accessible within class 'Foo'.
@publicis always implied and can be left off, but means that a property can be reached from anywhere.@privatemeans that a property can only be used within the containing class.@protectedmeans that a property can only be used within the containing class, and all derived subclasses, but not on dissimilar instances of the containing class.
Next, we’ve also added the@readonly modifier to ensure that a property is only ever written to during initialization.
js// @ts-checkclassFoo {constructor() {/**@readonly */this.stuff =100;}writeToStuff() {this.stuff =200;// ~~~~~// Cannot assign to 'stuff' because it is a read-only property.}}newFoo().stuff++;// ~~~~~// Cannot assign to 'stuff' because it is a read-only property.
Better Directory Watching on Linux andwatchOptions
TypeScript 3.8 ships a new strategy for watching directories, which is crucial for efficiently picking up changes tonode_modules.
For some context, on operating systems like Linux, TypeScript installs directory watchers (as opposed to file watchers) onnode_modules and many of its subdirectories to detect changes in dependencies.This is because the number of available file watchers is often eclipsed by the number of files innode_modules, whereas there are way fewer directories to track.
Older versions of TypeScript wouldimmediately install directory watchers on folders, and at startup that would be fine; however, during an npm install, a lot of activity will take place withinnode_modules and that can overwhelm TypeScript, often slowing editor sessions to a crawl.To prevent this, TypeScript 3.8 waits slightly before installing directory watchers to give these highly volatile directories some time to stabilize.
Because every project might work better under different strategies, and this new approach might not work well for your workflows, TypeScript 3.8 introduces a newwatchOptions field intsconfig.json andjsconfig.json which allows users to tell the compiler/language service which watching strategies should be used to keep track of files and directories.
{// Some typical compiler options"": {"":"es2020","":"node"// ...},// NEW: Options for file/directory watching"watchOptions": {// Use native file system events for files and directories"":"useFsEvents","":"useFsEvents",// Poll files for updates more frequently// when they're updated a lot."":"dynamicPriority"}}
watchOptions contains 4 new options that can be configured:
watchFile: the strategy for how individual files are watched. This can be set tofixedPollingInterval: Check every file for changes several times a second at a fixed interval.priorityPollingInterval: Check every file for changes several times a second, but use heuristics to check certain types of files less frequently than others.dynamicPriorityPolling: Use a dynamic queue where less-frequently modified files will be checked less often.useFsEvents(the default): Attempt to use the operating system/file system’s native events for file changes.useFsEventsOnParentDirectory: Attempt to use the operating system/file system’s native events to listen for changes on a file’s containing directories. This can use fewer file watchers, but might be less accurate.
watchDirectory: the strategy for how entire directory trees are watched under systems that lack recursive file-watching functionality. This can be set to:fixedPollingInterval: Check every directory for changes several times a second at a fixed interval.dynamicPriorityPolling: Use a dynamic queue where less-frequently modified directories will be checked less often.useFsEvents(the default): Attempt to use the operating system/file system’s native events for directory changes.
fallbackPolling: when using file system events, this option specifies the polling strategy that gets used when the system runs out of native file watchers and/or doesn’t support native file watchers. This can be set tofixedPollingInterval:(See above.)priorityPollingInterval:(See above.)dynamicPriorityPolling:(See above.)synchronousWatchDirectory: Disable deferred watching on directories. Deferred watching is useful when lots of file changes might occur at once (e.g. a change innode_modulesfrom runningnpm install), but you might want to disable it with this flag for some less-common setups.
For more information on these changes,head over to GitHub to see the pull request to read more.
“Fast and Loose” Incremental Checking
TypeScript 3.8 introduces a new compiler option calledassumeChangesOnlyAffectDirectDependencies.When this option is enabled, TypeScript will avoid rechecking/rebuilding all truly possibly-affected files, and only recheck/rebuild files that have changed as well as files that directly import them.
For example, consider a filefileD.ts that importsfileC.ts that importsfileB.ts that importsfileA.ts as follows:
fileA.ts <- fileB.ts <- fileC.ts <- fileD.ts
In--watch mode, a change infileA.ts would typically mean that TypeScript would need to at least re-checkfileB.ts,fileC.ts, andfileD.ts.UnderassumeChangesOnlyAffectDirectDependencies, a change infileA.ts means that onlyfileA.ts andfileB.ts need to be re-checked.
In a codebase like Visual Studio Code, this reduced rebuild times for changes in certain files from about 14 seconds to about 1 second.While we don’t necessarily recommend this option for all codebases, you might be interested if you have an extremely large codebase and are willing to defer full project errors until later (e.g. a dedicated build via atsconfig.fullbuild.json or in CI).
For more details, you cansee the original pull request.
The TypeScript docs are an open source project. Help us improve these pagesby sending a Pull Request ❤
Last updated: Dec 16, 2025