TypeScript 3.8 将会带来了许多特性,其中包含一些新的或即将到来的 ECMAScript 特性、仅仅导入/导出声明语法等。html
为了能让咱们导入类型,TypeScript 重用了 JavaScript 导入语法。例如在下面的这个例子中,咱们确保 JavaScript 的值 doThing
以及 TypeScript 类型 Options
一同被导入node
// ./foo.ts
interface Options {
// ...
}
export function doThing(options: Options) {
// ...
}
// ./bar.ts
import { doThing, Options } from './foo.js';
function doThingBetter(options: Options) {
// do something twice as good
doThing(options);
doThing(options);
}
复制代码
这很方便的,由于在大多数的状况下,咱们没必要担忧导入了什么 —— 仅仅是咱们想导入的内容。react
不幸的是,这仅是由于一个被称之为「导入省略」的功能而起做用。当 TypeScript 输出一个 JavaScript 文件时,TypeScript 会识别出 Options
仅仅是看成了一个类型来使用,它将会删除 Options
git
// ./foo.js
export function doThing(options: Options) {
// ...
}
// ./bar.js
import { doThing } from './foo.js';
function doThingBetter(options: Options) {
// do something twice as good
doThing(options);
doThing(options);
}
复制代码
在一般状况下,这种行为都是比较好的。可是它会致使一些其余问题。github
首先,在一些场景下,TypeScript 会混淆导出的到底是一个类型仍是一个值。好比在下面的例子中, MyThing
到底是一个值仍是一个类型?typescript
import { MyThing } from './some-module.js';
export { MyThing };
复制代码
若是单从这个文件来看,咱们无从得知答案。若是 Mything
仅仅是一个类型,Babel 和 TypeScript 使用的 transpileModule
API 编译出的代码将没法正确工做,而且 TypeScript 的 isolatedModules
编译选项将会提示咱们,这种写法将会抛出错误。问题的关键在于,没有一种方式能识别它仅仅是个类型,以及是否应该删除它,所以「导入省略」并不够好。json
同时,这也存在另一个问题,TypeScript 导入省略将会去除只包含用于类型声明的导入语句。对于含有反作用的模块,这形成了明显的不一样行为。因而,使用者将会不得不添加一条额外的声明语句,来确保有反作用。api
// This statement will get erased because of import elision.
import { SomeTypeFoo, SomeOtherTypeBar } from './module-with-side-effects';
// This statement always sticks around.
import './module-with-side-effects';
复制代码
一个咱们看到的具体例子是出如今 Angularjs(1.x)中, services
须要在全局在注册(它是一个反作用),可是导入的 services
仅仅用于类型声明中。异步
// ./service.ts
export class Service {
// ...
}
register('globalServiceId', Service);
// ./consumer.ts
import { Service } from './service.js';
inject('globalServiceId', function(service: Service) {
// do stuff with Service
});
复制代码
结果 ./service.js
中的代码不会被执行,致使在运行时会被中断。async
为了不这类行为,咱们意识到在什么该被导入/删除方面,须要给使用者提供更细粒度的控制。
在 TypeScript 3.8 版本中,咱们添加了一个仅仅导入/导出声明语法来作为解决方式。
import type { SomeThing } from "./some-module.js";
export type { SomeThing };
复制代码
import type
仅仅导入被用于类型注解或声明的声明语句,它老是会被彻底删除,所以在运行时将不会留下任何代码。与此类似,export type
仅仅提供一个用于类型的导出,在 TypeScript 输出文件中,它也将会被删除。
值得注意的是,类在运行时具备值,在设计时具备类型。它的使用与上下文有关。当使用 import type
导入一个类时,你不能作相似于从它继承的操做。
import type { Component } from "react";
interface ButtonProps {
// ...
}
class Button extends Component<ButtonProps> {
// ~~~~~~~~~
// error! 'Component' only refers to a type, but is being used as a value here.
// ...
}
复制代码
若是在以前你使用过 Flow,它们的语法是类似的。一个不一样的地方是咱们添加了一个新的限制条件,来避免可能混淆的代码。
// Is only 'Foo' a type? Or every declaration in the import?
// We just give an error because it's not clear.
import type Foo, { Bar, Baz } from "some-module";
// ~~~~~~~~~~~~~~~~~~~~~~
// error! A type-only import can specify a default import or named bindings, but not both.
复制代码
与 import type
相关联,咱们提供来一个新的编译选项:importsNotUsedAsValues
,经过它能够来控制没被使用的导入语句将会被如何处理,它的名字是暂定的,可是它提供来三个不一样的选项。
remove
,这是如今的行为 —— 丢弃这些导入语句。这仍然是默认行为,没有破坏性的更改preserve
,它将会保留全部的语句,即便是历来没有被使用。它能够保留反作用error
,它将会保留全部的导入(与 preserve
选项相同)语句,可是当一个值的导入仅仅用于类型时将会抛出错误。若是你想确保没有意外导入任何值,这会是有用的,可是对于反作用,你仍然须要添加额外的导入语法。对于该特性的更多信息,参考该 PR。
TypeScript 3.8 支持在 ECMAScript 中处于 stage-3 中的私有字段。
class Person {
#name: string
constructor(name: string) {
this.#name = name;
}
greet() {
console.log(`Hello, my name is ${this.#name}!`);
}
}
let jeremy = new Person("Jeremy Bearimy");
jeremy.#name
// ~~~~~
// Property '#name' is not accessible outside class 'Person'
// because it has a private identifier.
复制代码
不一样于正常属性(甚至是使用 private
修饰符声明的属性),私有字段有一些须要记住的规则:
#
字符作为开始,一般,咱们也把这些称为私有名称。public
和 private
修饰符不能用于私有字段除了「hard privacy」,私有字段的另一个优势是咱们先前提到的惟一性。
正常的属性容易被子类所改写
class C {
foo = 10;
cHelper() {
return this.foo;
}
}
class D extends C {
foo = 20;
dHelper() {
return this.foo;
}
}
let instance = new D();
// 'this.foo' refers to the same property on each instance.
console.log(instance.cHelper()); // prints '20'
console.log(instance.dHelper()); // prints '20'
复制代码
使用私有字段时,你彻底没必要对此担忧,由于每一个私有字段,在所包含的类中,都是惟一的
class C {
#foo = 10;
cHelper() {
return this.#foo;
}
}
class D extends C {
#foo = 20;
dHelper() {
return this.#foo;
}
}
let instance = new D();
// 'this.#foo' refers to a different field within each class.
console.log(instance.cHelper()); // prints '10'
console.log(instance.dHelper()); // prints '20'
复制代码
另外有一个值得注意的地方,访问一个有其余类型的私有字段,都将致使 TypeError
。
class Square {
#sideLength: number;
constructor(sideLength: number) {
this.#sideLength = sideLength;
}
equals(other: any) {
return this.#sideLength === other.#sideLength;
}
}
const a = new Square(100);
const b = { 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));
复制代码
对于类属性来讲,JavaScript 老是容许使用者访问没被声明的属性,而 TypeScript 须要使用者在访问以前先定义声明。使用私有字段时,不管时 .js
文件仍是 .ts
,都须要先声明。
class C {
/** @type {number} */
#foo;
constructor(foo: number) {
// This works.
this.#foo = foo;
}
}
复制代码
更多信息,请查看此 PR。
咱们已经收到不少关于「我该使用 private
关键字,仍是使用 ECMAScript 提供的私有字段 #
了?」这类的问题。
像全部其余好的问题同样,答案老是使人遗憾的:它取决你。
在属性方面,TypeScript private
修饰符在编译后将会被删除 —— 所以,尽管有数据存在,可是在输出的 JavaScript 代码中没有关于该属性声明的任何编码。在运行时,它的行为就像一个普通的属性。当你使用 private
关键字时,私有属性的有关行为只会出如今编译阶段/设计阶段,而对于 JavaScript 消费者来讲,则是彻底无感知的。
class C {
private foo = 10;
}
// This is an error at compile time,
// but when TypeScript outputs .js files,
// it'll run fine and print '10'.
console.log(new C().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(new C()['foo']); // prints '10'
复制代码
另外一方面,ECMAScript 私有属性没法在类以外访问。
class C {
#foo = 10;
}
console.log(new C().#foo); // SyntaxError
// ~~~~
// TypeScript reports an error *and*
// this won't work at runtime!
console.log(new C()["#foo"]); // prints undefined
// ~~~~~~~~~~~~~~~
// TypeScript reports an error under 'noImplicitAny',
// and this prints 'undefined'.
复制代码
「hard privacy」对于确保没有人能使用你的任何内部变量是有用的,若是你是一个库的做者,移除或者重命名一个私有字段不会形成任何重大变化。
正如上文所述,使用 ECMAScript 的私有字段,建立子类会更容易,由于它们是真私有。当使用 ECMAScript 私有字段时,子类无需担忧字段名字的冲突。当使用 TypeScript private
属性声明时,使用者仍然须要当心不要覆盖父类中的相同字段。
最后,还有一些你须要考虑的事情,好比你打算让你的代码在哪运行?当前,TypeScript 只有在编译目标为 ECMAScript 2015(ES6)及其以上时,才能支持该私有字段。由于咱们在底层使用 WeakMaps
实现这种方法 —— WeakMaps
并不能以一种不会致使内存泄漏的方式 polyfill。对比而言,TypeScript 的 private
声明属性能在全部的编译目标下正常工做 —— 甚至是 ECMAScript 3。
export * as ns
语法如下方式很常见
import * as utilities from './utilities.js';
export { utilities };
复制代码
在 ECMAScript 2020 中,添加了一种新的语法来支持该模式:
export * as utilities from "./utilities.js";
复制代码
这是一次 JavaScript 代码质量的改进,TypeScript 3.8 实现了此语法。
当你的编译目标早于 es2020
时,TypeScript 将会按照第一个代码片断输出内容。
Top-Level await
大多数使用 JavaScript 提供 I/O(如 http 请求)的现代环境都是异步的,而且不少现代 API 都返回 Promise
。尽管它在使操做无阻塞方面有诸多优势,可是它确实在一些如读取文件或外部内容时,会让人厌烦。
fetch('...')
.then(response => response.text())
.then(greeting => {
console.log(greeting);
});
复制代码
为了不 Promise
中 .then
的链式操做符,JavaScript 使用者一般会引入 async
函数以使用 await
,而后在定义该函数以后,当即调用该函数。
async function main() {
const response = await fetch('...');
const greeting = await response.text();
console.log(greeting);
}
main().catch(e => console.error(e));
复制代码
为了不引入 async
函数,咱们可使用一个简便的语法,它在即将到来的 ECMAScript feature 中被称为 top-level await
。
在当前的 JavaScript 中(以及其余具备类似功能的大多数其余语言),await
仅仅只能用于 async
函数内部。然而,使用 top-level await
时,咱们能够在一个模块的顶层使用 await
。
const response = await fetch('...');
const greeting = await response.text();
console.log(greeting);
// Make sure we're a module
export {};
复制代码
这里有一个细节:top-level await
仅仅只能在一个模块的顶层工做 —— 仅当 TypeScript 发现文件代码中含有 export
或者 import
时,才会认为该文件是一个模块。在一些基础的实践中,你可能须要写下 export {}
作为样板,来确保这种行为。
top-level await
并不会在你可能指望的全部环境下工做。如今,只有在编译目标选项是 es2017
及其以上,top-level await
才能被使用,而且 module
选项必须为 esnext
或者 system
。
更多相关信息,请查看该 PR。
TypeScript 3.8 经过打开 allJs
选项,能支持 JavaScript 文件,而且当使用 checkJs
选项或者在你的 .js
文件顶部中添加 // @ts-check
注释时,TypeScript 能对这些 .js
文件进行类型检查。
因为 JavaScript 文件没有专用的语法来进行类型检查,所以 TypeScript 选择利用 JSDoc。TypeScript 3.8 能理解一些新的 JSDoc 属性标签。
首先是全部的访问修饰符:@public
、@private
、@protected
。这些标签的工做方式与 TypeScript 中 public
、private
、protected
相同。
// @ts-check
class Foo {
constructor() {
/** @private */
this.stuff = 100;
}
printStuff() {
console.log(this.stuff);
}
}
new Foo().stuff;
// ~~~~~
// error! Property 'stuff' is private and only accessible within class 'Foo'.
复制代码
@public
是默认的,能够省略,它表明了一个属性能够从任何地方访问它@private
表示一个属性只能在包含的类中访问@protected
表示该属性只能在所包含的类及子类中访问,但不能在类的实例中访问下一步,咱们计划添加 @readonly
修饰符,来确保一个属性只能在初始化时被修改:
// @ts-check
class Foo {
constructor() {
/** @readonly */
this.stuff = 100;
}
writeToStuff() {
this.stuff = 200;
// ~~~~~
// Cannot assign to 'stuff' because it is a read-only property.
}
}
new Foo().stuff++;
// ~~~~~
// Cannot assign to 'stuff' because it is a read-only property.
复制代码
一直以来,TypeScript 致力于在 --watch
模式下和编辑器中提供可靠的文件监听功能。尽管在大部分状况下,它都能很好的工做,可是在 Node.js 中,文件监控很是困难,这主要体如今咱们的代码逻辑中。在 Node.js 中内置的 API 中,要么占用大量的 CPU 资源,要么不许确(fs.watchFile),甚至它们在各个平台的行为不一致(fs.watch)。除此以外,咱们几乎不可能肯定哪一个 API 会更好的工做,由于它们不只依赖于平台,还取决于文件所在的文件系统。
这一直是个难题,由于 TypeScript 须要在更多平台上运行,而不只仅是 Node.js。而且须要考虑到避免依赖模块彻底独立。这尤为适用于对 Node.js 原生模块有依赖的模块。
因为每一个项目在不一样的策略下均可能更好的工做,TypeScript 3.8 在 tsconfig.json
和 jsconfig.json
中添加了一个新的 watchOptions
字段,它可让使用者告诉编译器/语言服务,应该使用哪一种监听策略来跟踪文件或目录。
{
// Some typical compiler options
"compilerOptions": {
"target": "es2020",
"moduleResolution": "node",
// ...
},
// NEW: Options for file/directory watching
"watchOptions": {
// Use native file system events for files and directories
"watchFile": "useFsEvents",
"watchDirectory": "useFsEvents",
// Poll files for updates more frequently
// when they're updated a lot.
"fallbackPolling": "dynamicPriority"
}
}
复制代码
watchOptions
包含四种新的选项
watchFile
:监听单个文件的策略,它能够有如下值
fixedPollingInterval
,以固定的时间间隔,检查文件的更改priorityPollingInterval
,以固定的时间间隔,检查文件的更改,可是使用「heuristics」检查某些类型的文件的频率比其余文件低(heuristics 怎么翻?)dynamicPriorityPolling
,使用动态队列,在该队列中,较少检查不常常修改的文件useFsEvents
(默认),尝试使用操做系统/文件系统原生事件来监听文件更改useFsEventsOnParentDirectory
,尝试使用操做系统/文件系统原生事件来监听文件、目录的更改,这样可使用较小的文件监听程序,可是准确性可能较低watchDirectory
,在缺乏递归文件监听功能的系统中,使用哪一种策略监听整个目录树,它能够有如下值
fixedPollingInterval
,以固定的时间间隔,检查目录树的更改dynamicPriorityPolling
,使用动态队列,在该队列中,较少检查不常常修改的目录useFsEvents
(默认),尝试使用操做系统/文件系统原生事件来监听目录更改fallbackPolling
,当使用文件系统的事件,该选项用来指定使用特定策略,它能够有如下值
fixedPollingInterval
,同上priorityPollingInterval
,同上dynamicPriorityPolling
,同上synchronousWatchDirectory
,在目录上禁用延迟监听功能。在可能一次发生大量文件(如 node_modules)更改时,它很是有用,可是你可能须要一些不太常见的设置时,禁用它。