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)更改時,它很是有用,可是你可能須要一些不太常見的設置時,禁用它。