想象一下在 JavaScript 中有一個(gè)叫 concat
的函數(shù),它接受兩個(gè)數(shù)組或元組類型并將它們連接起來創(chuàng)建一個(gè)新數(shù)組。
function concat(arr1, arr2) {
return [...arr1, ...arr2];
}
還有一個(gè) tail
函數(shù),它接受一個(gè)數(shù)組或元組并返回除第一個(gè)元素外的所有元素。
function tail(arg) {
const [_, ...result] = arg;
return result;
}
在 TypeScript 中如何為這些函數(shù)添加類型呢?對(duì)于 concat
,在舊版本的語言中我們只能嘗試寫一些重載。
function concat(arr1: [], arr2: []): [];
function concat<A>(arr1: [A], arr2: []): [A];
function concat<A, B>(arr1: [A, B], arr2: []): [A, B];
function concat<A, B, C>(arr1: [A, B, C], arr2: []): [A, B, C];
function concat<A, B, C, D>(arr1: [A, B, C, D], arr2: []): [A, B, C, D];
function concat<A, B, C, D, E>(arr1: [A, B, C, D, E], arr2: []): [A, B, C, D, E];
function concat<A, B, C, D, E, F>(arr1: [A, B, C, D, E, F], arr2: []): [A, B, C, D, E, F];
這僅僅是當(dāng)?shù)诙€(gè)數(shù)組為空時(shí)的七種重載。如果 arr2
有一個(gè)參數(shù),還需要添加更多重載。
TypeScript 4.0 引入了兩個(gè)基本更改以及推斷改進(jìn),使這些類型成為可能。
第一個(gè)更改是元組類型語法中的擴(kuò)展現(xiàn)在可以是泛型。這意味著我們可以在不知道實(shí)際類型的情況下,表示對(duì)元組和數(shù)組的高階操作。當(dāng)在這些元組類型中實(shí)例化泛型擴(kuò)展時(shí),它們可以生成其他數(shù)組和元組類型。
第二個(gè)更改是元組中的剩余元素可以出現(xiàn)在任何位置,而不僅僅是末尾!
通過結(jié)合這兩種行為,我們可以為 concat
編寫一個(gè)單一的、類型良好的簽名:
type Arr = readonly any[];
function concat<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U] {
return [...arr1, ...arr2];
}
雖然這個(gè)簽名仍然有點(diǎn)長(zhǎng),但它只是一個(gè)不需要重復(fù)的簽名,并且在所有數(shù)組和元組上都能提供可預(yù)測(cè)的行為。
改進(jìn)元組類型和參數(shù)列表的體驗(yàn)很重要,因?yàn)樗梢宰屛覀冊(cè)诔R姷?JavaScript 習(xí)慣用法周圍獲得強(qiáng)類型驗(yàn)證。使用元組類型作為剩余參數(shù)就是一個(gè)關(guān)鍵點(diǎn)。
例如,以下使用元組類型作為剩余參數(shù)的函數(shù):
function foo(...args: [string, number]): void {
// ...
}
對(duì)于任何調(diào)用 foo
的人來說,應(yīng)該與以下函數(shù)沒有區(qū)別:
function foo(arg0: string, arg1: number): void {
// ...
}
在第一個(gè)例子中,我們沒有為第一個(gè)和第二個(gè)元素提供參數(shù)名。雖然這對(duì)類型檢查沒有影響,但缺少標(biāo)簽的元組位置可能使它們更難使用,更難傳達(dá)我們的意圖。
這就是為什么在 TypeScript 4.0 中,元組類型現(xiàn)在可以提供標(biāo)簽。
type Range = [start: number, end: number];
為了加深參數(shù)列表和元組類型之間的聯(lián)系,剩余元素和可選元素的語法鏡像了參數(shù)列表的語法。
type Foo = [first: number, second?: string, ...rest: any[]];
使用帶標(biāo)簽的元組時(shí),需要注意一些規(guī)則。例如,當(dāng)為元組元素添加標(biāo)簽時(shí),元組中的其他元素也必須有標(biāo)簽。
標(biāo)簽不會(huì)要求我們?cè)诮鈽?gòu)時(shí)以不同的方式命名變量。它們僅用于文檔和工具。
當(dāng)啟用 noImplicitAny
時(shí),TypeScript 4.0 現(xiàn)在可以使用控制流分析來確定類中屬性的類型。
class Square {
area;
sideLength;
constructor(sideLength: number) {
this.sideLength = sideLength;
this.area = sideLength ** 2;
}
}
在構(gòu)造函數(shù)并非所有路徑都分配給實(shí)例成員的情況下,屬性可能被視為 undefined
。
class Square {
sideLength;
constructor(sideLength: number) {
if (Math.random()) {
this.sideLength = sideLength;
}
}
get area() {
return this.sideLength ** 2;
}
}
在你更了解情況時(shí)(例如,你有一個(gè) initialize
方法),仍然需要顯式的類型注解以及在 strictPropertyInitialization
下的確定性賦值斷言。
class Square {
sideLength!: number;
constructor(sideLength: number) {
this.initialize(sideLength);
}
initialize(sideLength: number) {
this.sideLength = sideLength;
}
get area() {
return this.sideLength ** 2;
}
}
JavaScript 和許多其他語言支持一組稱為復(fù)合賦值運(yùn)算符的運(yùn)算符。復(fù)合賦值運(yùn)算符對(duì)兩個(gè)參數(shù)應(yīng)用運(yùn)算符,然后將結(jié)果賦值給左側(cè)。你可能見過這些:
// 加法
a += b;
// 減法
a -= b;
// 乘法
a *= b;
// 除法
a /= b;
// 指數(shù)運(yùn)算
a **= b;
// 左移位
a <<= b;
直到最近,還有三個(gè)顯著的例外:邏輯與(&&
)、邏輯或(||
)和 nullish 合并(??
)。
這就是為什么 TypeScript 4.0 支持 ECMAScript 的新功能,添加了三個(gè)新的賦值運(yùn)算符:&&=
、||=
和 ??=
。
這些運(yùn)算符非常適合替代用戶可能編寫的代碼,例如:
a = a && b;
a = a || b;
a = a ?? b;
或者類似的 if
塊:
if (!a) {
a = b;
}
還有一些我們見過的(或者我們自己寫過的)懶加載值的模式,只有在需要時(shí)才會(huì)初始化。
let values: string[];
(values ?? (values = [])).push("hello");
// 之后
(values ??= []).push("hello");
在罕見情況下,如果你使用帶有副作用的 getter 或 setter,需要注意這些運(yùn)算符只在必要時(shí)執(zhí)行賦值。從這個(gè)意義上說,不僅運(yùn)算符的右側(cè)被“短路”,賦值本身也被短路。
const obj = {
get prop() {
console.log("getter has run");
return Math.random() < 0.5;
},
set prop(_val: boolean) {
console.log("setter has run");
}
};
function foo() {
console.log("right side evaluated");
return true;
}
console.log("This one always runs the setter");
obj.prop = obj.prop || foo();
console.log("This one *sometimes* runs the setter");
obj.prop ||= foo();
從 TypeScript 誕生之初,catch 子句變量一直被類型化為 any
。這意味著 TypeScript 允許你對(duì)它們做任何事情。
try {
// 做一些工作
} catch (x) {
// x 的類型是 'any' - 玩得開心!
console.log(x.message);
console.log(x.toUpperCase());
x++;
x.yadda.yadda.yadda();
}
如果我們?cè)噲D在錯(cuò)誤處理代碼中防止更多錯(cuò)誤發(fā)生,這可能會(huì)帶來一些不良行為!因?yàn)檫@些變量默認(rèn)具有 any
類型,它們?nèi)狈θ魏晤愋桶踩?,可能?huì)在無效操作上出錯(cuò)。
這就是為什么 TypeScript 4.0 現(xiàn)在允許你將 catch 子句變量的類型指定為 unknown
。unknown
比 any
更安全,因?yàn)樗嵝盐覀冃枰诓僮髦抵斑M(jìn)行某種類型檢查。
try {
// ...
} catch (e: unknown) {
// 不能訪問 unknown 類型的值
console.log(e.toUpperCase());
if (typeof e === "string") {
// 我們已經(jīng)將 'e' 窄化為 'string' 類型
console.log(e.toUpperCase());
}
}
雖然 catch 變量的類型不會(huì)默認(rèn)改變,但我們可能會(huì)考慮在未來添加一個(gè)新的 strict
模式標(biāo)志,以便用戶可以選擇這種行為。同時(shí),應(yīng)該可以編寫一個(gè) lint 規(guī)則來強(qiáng)制 catch 變量具有顯式的 : any
或 : unknown
注解。
在使用 JSX 時(shí),片段是一種允許我們返回多個(gè)子元素的 JSX 元素類型。當(dāng)我們首次在 TypeScript 中實(shí)現(xiàn)片段時(shí),我們對(duì)其他庫(kù)如何利用它們沒有很好的想法。如今,大多數(shù)鼓勵(lì)使用 JSX 并支持片段的庫(kù)都有類似的 API 形狀。
在 TypeScript 4.0 中,用戶可以通過新的 jsxFragmentFactory
選項(xiàng)自定義片段工廠。
例如,以下 tsconfig.json
文件告訴 TypeScript 以與 React 兼容的方式轉(zhuǎn)換 JSX,但將每個(gè)工廠調(diào)用切換為 h
而不是 React.createElement
,并使用 Fragment
而不是 React.Fragment
。
{
"": {
"": "esnext",
"": "commonjs",
"": "react",
"": "h",
"": "Fragment"
}
}
在需要在每個(gè)文件中使用不同的 JSX 工廠的情況下,可以利用新的 /** @jsxFrag */
注釋 pragma。例如,以下代碼:
// 注意:這些注釋 pragma 需要以 JSDoc 風(fēng)格的多行語法編寫才能生效
/** @jsx h */
/** @jsxFrag Fragment */
import { h, Fragment } from "preact";
export const Header = (
<>
<h1>Welcome</h1>
</>
);
將被轉(zhuǎn)換為以下輸出 JavaScript:
import React from 'react';
export const Header = React.createElement(
React.Fragment,
null,
React.createElement("h1", null, "Welcome")
);
以前,在 incremental
模式下使用 noEmitOnError
標(biāo)志編譯一個(gè)有錯(cuò)誤的程序會(huì)非常慢。這是因?yàn)樯洗尉幾g的信息不會(huì)基于 noEmitOnError
標(biāo)志緩存在 .tsbuildinfo
文件中。
TypeScript 4.0 改變了這一點(diǎn),在這些場(chǎng)景中提供了巨大的速度提升,并且反過來改進(jìn)了 --build
模式(隱含 incremental
和 noEmitOnError
)。
TypeScript 4.0 允許我們?cè)谑褂?noEmit
標(biāo)志的同時(shí)仍然利用增量編譯。這以前是不允許的,因?yàn)?incremental
需要生成 .tsbuildinfo
文件;然而,啟用更快增量構(gòu)建的用例對(duì)于所有用戶來說都足夠重要。
TypeScript 編譯器不僅為大多數(shù)主要編輯器中的 TypeScript 本身提供編輯體驗(yàn),還為 Visual Studio 系列編輯器中的 JavaScript 體驗(yàn)提供支持。因此,我們的很多工作都集中在改進(jìn)編輯器場(chǎng)景上——這是你作為開發(fā)人員花費(fèi)大部分時(shí)間的地方。
在編輯器中使用新的 TypeScript/JavaScript 功能會(huì)因編輯器而異,但
你可以查看支持 TypeScript 的編輯器列表,了解更多你最喜歡的編輯器是否支持使用新版本。
可選鏈?zhǔn)秸{(diào)用是一個(gè)最近受到很多喜愛的功能。這就是為什么 TypeScript 4.0 帶來了一個(gè)新的重構(gòu),將常見的模式轉(zhuǎn)換為利用可選鏈?zhǔn)秸{(diào)用和 nullish 合并!
TypeScript 的編輯支持現(xiàn)在可以識(shí)別聲明是否被 /** @deprecated */
JSDoc 注釋標(biāo)記。這些信息會(huì)在完成功能列表中顯示,并作為編輯器可以特別處理的建議診斷。在像 VS Code 這樣的編輯器中,已棄用的值通常會(huì)以刪除線樣式顯示。
我們聽到了很多用戶在大型項(xiàng)目中啟動(dòng)時(shí)間過長(zhǎng)的反饋。罪魁禍?zhǔn)淄ǔJ欠Q為程序構(gòu)建的過程。這是從初始根文件集開始,解析它們,查找它們的依賴項(xiàng),解析這些依賴項(xiàng),查找這些依賴項(xiàng)的依賴項(xiàng),依此類推的過程。項(xiàng)目越大,你等待基本編輯操作(如轉(zhuǎn)到定義或快速信息)的時(shí)間就越長(zhǎng)。
這就是為什么我們一直在為編輯器開發(fā)一種新的模式,在完整語言服務(wù)體驗(yàn)加載之前提供部分體驗(yàn)。核心思想是編輯器可以運(yùn)行一個(gè)輕量級(jí)的部分服務(wù)器,只查看編輯器當(dāng)前打開的文件。
很難確切地說你會(huì)看到什么樣的改進(jìn),但根據(jù)經(jīng)驗(yàn),以前在 Visual Studio Code 代碼庫(kù)上,TypeScript 完全響應(yīng)需要 20 秒到一分鐘。相比之下,我們的新部分語義模式似乎將延遲縮短到只需幾秒鐘。
目前,只有 Visual Studio Code 支持這種模式,并且在 Visual Studio Code Insiders 中有一些用戶體驗(yàn)改進(jìn)即將推出。我們認(rèn)識(shí)到這種體驗(yàn)在用戶體驗(yàn)和功能上可能仍有改進(jìn)空間,我們有一系列改進(jìn)想法。我們希望獲得更多關(guān)于你認(rèn)為可能有用的反饋。
自動(dòng)導(dǎo)入是一個(gè)讓編碼變得更加容易的出色功能,但每當(dāng)自動(dòng)導(dǎo)入似乎無法正常工作時(shí),都會(huì)讓用戶感到困惑。我們從用戶那里聽到的一個(gè)具體問題是,自動(dòng)導(dǎo)入在 TypeScript 編寫的依賴項(xiàng)上無法正常工作,直到他們?cè)陧?xiàng)目中的其他地方至少顯式導(dǎo)入了一次。
為什么自動(dòng)導(dǎo)入對(duì) @types
包有效,但對(duì)自帶類型的包無效?事實(shí)證明,自動(dòng)導(dǎo)入只對(duì)你的項(xiàng)目已經(jīng)包含的包有效。因?yàn)?TypeScript 有一些奇怪的默認(rèn)行為,會(huì)自動(dòng)將 node_modules/@types
中的包添加到你的項(xiàng)目中,所以這些包會(huì)被自動(dòng)導(dǎo)入。另一方面,其他包被排除在外,因?yàn)楸闅v所有 node_modules
包可能會(huì)非常昂貴。
所有這些都導(dǎo)致了一個(gè)很糟糕的初始體驗(yàn),當(dāng)你試圖自動(dòng)導(dǎo)入一個(gè)剛剛安裝但尚未使用過的包時(shí)。
TypeScript 4.0 現(xiàn)在在編輯器場(chǎng)景中做了一些額外的工作,以包含你在 package.json
的 dependencies
(和 peerDependencies
)字段中列出的包。這些包中的信息僅用于改進(jìn)自動(dòng)導(dǎo)入,不會(huì)改變類型檢查等其他任何內(nèi)容。這允許我們?yōu)樗袔в蓄愋偷囊蕾図?xiàng)提供自動(dòng)導(dǎo)入,而無需承擔(dān)完整 node_modules
搜索的成本。
在罕見情況下,當(dāng)你的 package.json
列出了超過十個(gè)尚未導(dǎo)入的帶類型依賴項(xiàng)時(shí),此功能會(huì)自動(dòng)禁用,以防止項(xiàng)目加載緩慢。要強(qiáng)制啟用此功能或完全禁用它,你應(yīng)該能夠配置你的編輯器。對(duì)于 Visual Studio Code,這是“包含包 JSON 自動(dòng)導(dǎo)入”(或 typescript.preferences.includePackageJsonAutoImports
)設(shè)置。
TypeScript 網(wǎng)站最近已經(jīng)從頭重寫并推出!
我們已經(jīng)寫了一些關(guān)于新網(wǎng)站的內(nèi)容,所以你可以在那里了解更多;但值得一提的是,我們?nèi)匀幌M牭侥愕南敕ǎ∪绻阌腥魏螁栴}、評(píng)論或建議,可以在網(wǎng)站的問題跟蹤器上提交。
我們的 lib.d.ts
聲明已更改——最具體來說,DOM 的類型已更改。最顯著的更改可能是移除了 document.origin
,它僅在舊版本的 IE 和 Safari 中有效。MDN 建議遷移到 self.origin
。
以前,只有在使用 useDefineForClassFields
時(shí),屬性覆蓋訪問器或訪問器覆蓋屬性才會(huì)報(bào)錯(cuò);然而,TypeScript 現(xiàn)在在派生類中聲明會(huì)覆蓋基類中的 getter 或 setter 的屬性時(shí)始終會(huì)報(bào)錯(cuò)。
class Base {
get foo() {
return 100;
}
set foo(value) {
// ...
}
}
class Derived extends Base {
foo = 10;
}
class Base {
prop = 10;
}
class Derived extends Base {
get prop() {
return 100;
}
}
在 strictNullChecks
中使用 delete
運(yùn)算符時(shí),操作數(shù)現(xiàn)在必須是 any
、unknown
、never
或是可選的(即類型中包含 undefined
)。否則,使用 delete
運(yùn)算符會(huì)報(bào)錯(cuò)。
interface Thing {
prop: string;
}
function f(x: Thing) {
delete x.prop;
}
如今,TypeScript 提供了一組用于生成 AST 節(jié)點(diǎn)的“工廠”函數(shù);然而,TypeScript 4.0 提供了一個(gè)新的節(jié)點(diǎn)工廠 API。因此,對(duì)于 TypeScript 4.0,我們決定棄用這些舊函數(shù),以支持新的函數(shù)。
更多建議: