TypeScript 5-0 正式發佈!
2023 年 3 月 17 日,TypeScript 5.0 正式發佈!此版本帶來了許多新功能,旨在使 TypeScript 更小、更簡單、更快。TypeScript 5.0 實現了新的裝飾器標準、更好地支持 Node 和打構建工具中的 ESM 項目的功能、庫作者控制泛型推導的新方法、擴展了 JSDoc 功能、簡化了配置,並進行了許多其他改進。
可以通過以下 npm 命令開始使用 TypeScript 5.0:
npm install -D typescript
以下是 TypeScript 5.0 的主要更新:
-
全新裝飾器
-
const
類型參數 -
extends
支持多配置文件 -
所有枚舉都是聯合枚舉
-
--moduleResolution
bundler -
自定義解析標誌
-
--verbatimModuleSyntax
-
支持
export type *
-
JSDoc 支持
@satisfies
-
JSDoc 支持
@overload
-
編輯器中不區分大小寫的導入排序
-
完善
switch/case
-
優化速度、內存和包大小
-
其他重大更改和棄用
全新裝飾器
裝飾器是即將推出的 ECMAScript 特性,它允許我們以可重用的方式自定義類及其成員。
考慮以下代碼:
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
const p = new Person("Ray");
p.greet();
這裏的 greet
方法很簡單,在實際中它內部可能會跟複雜,比如需要執行異步邏輯,或者進行遞歸,亦或是有副作用等。那就可能需要使用 console.log
來調試 greet
:
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
greet() {
console.log("LOG: Entering method.");
console.log(`Hello, my name is ${this.name}.`);
console.log("LOG: Exiting method.")
}
}
如果有一種方法可以爲每種方法做到這一點,可能會很好。
這就是裝飾器的用武之地。我們可以編寫一個名爲 loggedMethod
的函數,如下所示:
function loggedMethod(originalMethod: any, _context: any) {
function replacementMethod(this: any, ...args: any[]) {
console.log("LOG: Entering method.")
const result = originalMethod.call(this, ...args);
console.log("LOG: Exiting method.")
return result;
}
return replacementMethod;
}
這裏用了很多 any,可以暫時忽略,這樣可以讓例子儘可能得簡單。
這裏,loggedMethod
需要傳入一個參數 (originalMethod
) 並返回一個函數。執行過程如下:
-
打印:LOG: Entering method.
-
將 this 及其所有參數傳遞給原始方法
-
打印:LOG: Exiting method.
-
返回原始方法的執行結果
現在我們就可以使用 loggedMethod
來修飾 greet
方法:
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
@loggedMethod
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
const p = new Person("Ray");
p.greet();
輸出如下:
LOG: Entering method.
Hello, my name is Ray.
LOG: Exiting method.
這裏我們在 greet
上面使用了 loggedMethod
作爲裝飾器——注意這裏的寫法:@loggedMethod
。這樣,它會被原始方法和 context
對象調用。因爲 loggedMethod
返回了一個新函數,該函數替換了 greet
的原始定義。
loggedMethod 的第二個參數被稱爲 “ context
對象”,它包含一些關於如何聲明裝飾方法的有用信息——比如它是 #private
成員還是靜態成員,或者方法的名稱是什麼。 下面來重寫 loggedMethod 以利用它並打印出被修飾的方法的名稱。
function loggedMethod(originalMethod: any, context: ClassMethodDecoratorContext) {
const methodName = String(context.name);
function replacementMethod(this: any, ...args: any[]) {
console.log(`LOG: Entering method '${methodName}'.`)
const result = originalMethod.call(this, ...args);
console.log(`LOG: Exiting method '${methodName}'.`)
return result;
}
return replacementMethod;
}
TypeScript 提供了一個名爲 ClassMethodDecoratorContext
的類型,它對方法裝飾器採用的 context
對象進行建模。除了元數據之外,方法的 context
對象還有一個有用的函數:addInitializer
。 這是一種掛接到構造函數開頭的方法(如果使用靜態方法,則掛接到類本身的初始化)。
舉個例子,在 JavaScript 中,經常會寫如下的模式:
class Person {
name: string;
constructor(name: string) {
this.name = name;
this.greet = this.greet.bind(this);
}
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
或者,greet
可以聲明爲初始化爲箭頭函數的屬性。
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
greet = () => {
console.log(`Hello, my name is ${this.name}.`);
};
}
編寫這段代碼是爲了確保在greet
作爲獨立函數調用或作爲回調函數傳遞時不會重新綁定。
const greet = new Person("Ray").greet;
greet();
可以編寫一個裝飾器,使用addInitializer
在構造函數中爲我們調用 bind
。
function bound(originalMethod: any, context: ClassMethodDecoratorContext) {
const methodName = context.name;
if (context.private) {
throw new Error(`'bound' cannot decorate private properties like ${methodName as string}.`);
}
context.addInitializer(function () {
this[methodName] = this[methodName].bind(this);
});
}
bound
不會返回任何內容,所以當它裝飾一個方法時,它會保留原來的方法。相反,它會在其他字段初始化之前添加邏輯。
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
@bound
@loggedMethod
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
const p = new Person("Ray");
const greet = p.greet;
greet();
注意,我們使用了兩個裝飾器:@bound
和@loggedMethod
。這些裝飾是以 “相反的順序” 運行的。也就是說,@loggedMethod
修飾了原始方法greet
, @bound
修飾了@loggedMethod
的結果。在這個例子中,這沒有關係——但如果裝飾器有副作用或期望某種順序,則可能有關係。
可以將這些裝飾器放在同一行:
@bound @loggedMethod greet() {
console.log(`Hello, my name is ${this.name}.`);
}
我們甚至可以創建返回裝飾器函數的函數。這使得我們可以對最終的裝飾器進行一些自定義。如果我們願意,我們可以讓loggedMethod
返回一個裝飾器,並自定義它記錄消息的方式。
function loggedMethod(headMessage = "LOG:") {
return function actualDecorator(originalMethod: any, context: ClassMethodDecoratorContext) {
const methodName = String(context.name);
function replacementMethod(this: any, ...args: any[]) {
console.log(`${headMessage} Entering method '${methodName}'.`)
const result = originalMethod.call(this, ...args);
console.log(`${headMessage} Exiting method '${methodName}'.`)
return result;
}
return replacementMethod;
}
}
如果這樣做,必須在使用loggedMethod
作爲裝飾器之前調用它。然後,可以傳入任何字符串作爲記錄到控制檯的消息的前綴。
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
@loggedMethod("")
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
const p = new Person("Ray");
p.greet();
輸出結果如下:
Entering method 'greet'.
Hello, my name is Ray.
Exiting method 'greet'.
裝飾器可不僅僅用於方法,還可以用於屬性 / 字段、getter
、setter
和自動訪問器。甚至類本身也可以裝飾成子類化和註冊。
上面的loggedMethod
和bound
裝飾器示例寫的很簡單,並省略了大量關於類型的細節。實際上,編寫裝飾器可能相當複雜。例如,上面的loggedMethod
類型良好的版本可能看起來像這樣:
function loggedMethod<This, Args extends any[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
const methodName = String(context.name);
function replacementMethod(this: This, ...args: Args): Return {
console.log(`LOG: Entering method '${methodName}'.`)
const result = target.call(this, ...args);
console.log(`LOG: Exiting method '${methodName}'.`)
return result;
}
return replacementMethod;
}
我們必須使用this
、Args
和return
類型參數分別建模 this、參數和原始方法的返回類型。
具體定義裝飾器函數的複雜程度取決於想要保證什麼。需要記住,裝飾器的使用次數將超過它們的編寫次數,所以類型良好的版本通常是更好的——但顯然與可讀性有一個權衡,所以請儘量保持簡單。
const 類型參數
當推斷一個對象的類型時,TypeScript 通常會選擇一個通用類型。例如,在本例中,names
的推斷類型是string[]
:
type HasNames = { readonly names: string[] };
function getNamesExactly<T extends HasNames>(arg: T): T["names"] {
return arg.names;
}
// names 的推斷類型爲 string[]
const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"]});
通常這樣做的目的是實現突變。然而,根據getnames
確切的作用以及它的使用方式,通常情況下需要更具體的類型。到目前爲止,通常不得不在某些地方添加const
,以實現所需的推斷:
// 我們想要的類型: readonly ["Alice", "Bob", "Eve"]
// 我們得到的類型: string[]
const names1 = getNamesExactly({ names: ["Alice", "Bob", "Eve"]});
// 得到想要的類型:readonly ["Alice", "Bob", "Eve"]
const names2 = getNamesExactly({ names: ["Alice", "Bob", "Eve"]} as const);
這寫起來會很麻煩,也很容易忘記。在 TypeScript 5.0 中,可以在類型參數聲明中添加const
修飾符,從而使類const
推斷成爲默認值:
type HasNames = { names: readonly string[] };
function getNamesExactly<const T extends HasNames>(arg: T): T["names"] {
// ^^^^^
return arg.names;
}
// 推斷類型:readonly ["Alice", "Bob", "Eve"]
// 注意,這裏不需要再寫 as const
const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"] });
注意,const
修飾符並不排斥可變值,也不需要不可變約束。使用可變類型約束可能會得到意外的結果。例如:
declare function fnBad<const T extends string[]>(args: T): void;
// T仍然是string[],因爲readonly ["a", "b", "c"]不能賦值給string[]
fnBad(["a", "b" ,"c"]);
這裏,T 的推斷候選值是readonly ["a", "b", "c"]
,而readonly
數組不能用於需要可變數組的地方。在這種情況下,推理回退到約束,數組被視爲string[]
,調用仍然成功進行。
更好的定義應該使用readonly string[]
:
declare function fnGood<const T extends readonly string[]>(args: T): void;
// T 是 readonly ["a", "b", "c"]
fnGood(["a", "b" ,"c"]);
同樣,要記住,const
修飾符隻影響在調用中編寫的對象、數組和基本類型表達式的推斷,所以不會 (或不能) 用const
修飾的參數將看不到任何行爲的變化:
declare function fnGood<const T extends readonly string[]>(args: T): void;
const arr = ["a", "b" ,"c"];
// T 仍然是 string[],const 修飾符沒有作用
fnGood(arr);
extends 支持多配置文件
當管理多個項目時,通常每個項目的 tsconfig.json
文件都會繼承於基礎配置。這就是爲什麼 TypeScript 支持extends
字段,用於從compilerOptions
中複製字段。
// packages/front-end/src/tsconfig.json
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "../lib",
// ...
}
}
但是,在某些情況下,可能希望從多個配置文件進行擴展。例如,想象一下使用一個 TypeScript 基本配置文件到 npm。如果想讓所有的項目也使用 npm 中@tsconfig/strictest
包中的選項,那麼有一個簡單的解決方案: 將tsconfig.base.json
擴展到@tsconfig/strictest
:
// tsconfig.base.json
{
"extends": "@tsconfig/strictest/tsconfig.json",
"compilerOptions": {
// ...
}
}
這在一定程度上是有效的。 如果有任何項目不想使用 @tsconfig/strictest
,就必須手動禁用這些選項,或者創建一個不從 @tsconfig/strictest
擴展的單獨版本的 tsconfig.base.json
。
爲了提供更多的靈活性,Typescript 5.0 允許extends
字段接收多個項。例如,在這個配置文件中:
{
"extends": ["a", "b", "c"],
"compilerOptions": {
// ...
}
}
這樣寫有點像直接擴展 c,其中 c 擴展 b,b 擴展 a。 如果任何字段 “衝突”,則後一個項生效。
所以在下面的例子中,strictNullChecks
和 noImplicitAny
都會在最終的 tsconfig.json
中啓用。
// tsconfig1.json
{
"compilerOptions": {
"strictNullChecks": true
}
}
// tsconfig2.json
{
"compilerOptions": {
"noImplicitAny": true
}
}
// tsconfig.json
{
"extends": ["./tsconfig1.json", "./tsconfig2.json"],
"files": ["./index.ts"]
}
可以用下面的方式重寫最上面的例子:
// packages/front-end/src/tsconfig.json
{
"extends": ["@tsconfig/strictest/tsconfig.json", "../../../tsconfig.base.json"],
"compilerOptions": {
"outDir": "../lib",
// ...
}
}
所有枚舉都是聯合枚舉
當 TypeScript 最初引入枚舉時,它只不過是一組具有相同類型的數值常量:
enum E {
Foo = 10,
Bar = 20,
}
E.Foo 和 E.Bar 唯一的特別之處在於它們可以分配給任何期望類型 E 的東西。除此之外,它們只是數字。
function takeValue(e: E) {}
takeValue(E.Foo); // ✅
takeValue(123); // ❌
直到 TypeScript 2.0 引入了枚舉字面量類型,它賦予每個枚舉成員自己的類型,並將枚舉本身轉換爲每個成員類型的聯合。它還允許我們只引用枚舉類型的一個子集,並縮小這些類型。
// Color就像是一個聯合:Red | Orange | Yellow | Green | Blue | Violet
enum Color {
Red, Orange, Yellow, Green, Blue, /* Indigo */, Violet
}
// 每個枚舉成員都有自己的類型,可以引用
type PrimaryColor = Color.Red | Color.Green | Color.Blue;
function isPrimaryColor(c: Color): c is PrimaryColor {
// 縮小字面量類型可以捕獲bug
// TypeScript在這裏會報錯,因爲
// 最終會比較 Color.Red 和 Color.Green。
// 本想使用||,但不小心寫了&&
return c === Color.Red && c === Color.Green && c === Color.Blue;
}
給每個枚舉成員指定自己的類型有一個問題,即這些類型在某種程度上與成員的實際值相關聯。在某些情況下,這個值是不可能計算出來的——例如,枚舉成員可以通過函數調用進行初始化。
enum E {
Blah = Math.random()
}
每當 TypeScript 遇到這些問題時,它都會悄無聲息地退出並使用舊的枚舉策略。這意味着要放棄並集和字面量類型的所有優點。
TypeScript 5.0 通過爲每個計算成員創建唯一的類型,設法將所有枚舉轉換爲聯合枚舉。這意味着現在可以縮小所有枚舉的範圍,並將其成員作爲類型引用。
--moduleResolution
TypeScript 4.7 爲 --module
和 --moduleResolution
設置引入了 node16 和 nodenext 選項。這些選項的目的是更好地模擬 Node.js 中 ECMAScript 模塊的精確查找規則; 然而,這種模式有許多其他工具沒有真正執行的限制。
例如,在 Node.js 的 ECMAScript 模塊中,任何相對導入都需要包含文件擴展名。
// entry.mjs
import * as utils from "./utils"; // ❌ - 需要包括文件擴展名。
import * as utils from "./utils.mjs"; // ✅
在 Node.js 和瀏覽器中這樣做是有原因的——它使文件查找更快,並且更適合原始文件服務器。但對於許多使用打包工具的開發人員來說,node16/nodenext 的設置很麻煩,因爲打包工具沒有這些限制中的大部分。在某些方面,node 解析模式更適合使用打包工具的人。
但在某些方面,原有的 node 解析模式已經過時了。 大多數現代打包工具在 Node.js 中使用 ECMAScript 模塊和 CommonJS 查找規則的融合。
爲了模擬打包工具是如何工作的,TypeScript 5.0 引入了一個新策略:--moduleResolution bundler
{
"compilerOptions": {
"target": "esnext",
"moduleResolution": "bundler"
}
}
如果正在使用現代打包工具,如 Vite、esbuild、swc、Webpack、Parcel 或其他實現混合查找策略的打包工具,那麼新的 bundler
選項應該非常適合你。
另一方面,如果正在編寫一個打算在 npm 上發佈的庫,使用bundler
選項可以隱藏不使用bundler
的用戶可能出現的兼容性問題。因此,在這些情況下,使用node16
或nodenext
解析選項可能是更好的方法。
自定義解析標誌
JavaScript 工具現在可以模擬 “混合” 解析規則,就像上面描述的打包工具模式一樣。 由於工具的支持可能略有不同,TypeScript 5.0 提供了啓用或禁用一些功能的方法。
allowImportingTsExtensions
--allowImportingTsExtensions
允許 TypeScript 文件使用特定於 TypeScript 的擴展名(如 .ts
、.mts
或 .tsx
)相互導入。
僅當啓用 --noEmit
或 --emitDeclarationOnly
時才允許使用此標誌,因爲這些導入路徑在運行時無法在 JavaScript 輸出文件中解析。 這裏的期望是解析器(例如打包工具、運行時或其他工具)將使 .ts
文件之間的這些導入正常工作。
resolvePackageJsonExports
--resolvePackageJsonExports
強制 TypeScript 在從 node_modules
中的包中讀取時查詢 package.json
文件的 exports
字段。
resolvePackageJsonImports
--resolvePackageJsonImports
強制 TypeScript 在從其祖先目錄包含 package.json
的文件執行以 #
開頭的查找時查詢 package.json
文件的 imports
字段。
在 --moduleResolution
的 node16
、nodenext
和 bundler
選項下,此選項默認爲 true。
allowArbitraryExtensions
在 TypeScript 5.0 中,當導入路徑以不是已知 JavaScript 或 TypeScript 文件擴展名的擴展名結尾時,編譯器將以 {file basename}.d.{extension}
的形式查找該路徑的聲明文件。例如,如果在打包項目中使用 CSS loader,可能希望爲這些樣式表編寫(或生成)聲明文件:
/* app.css */
.cookie-banner {
display: none;
}
// app.d.css.ts
declare const css: {
cookieBanner: string;
};
export default css;
// App.tsx
import styles from "./app.css";
styles.cookieBanner; // string
默認情況下,這個導入將引發一個錯誤,讓你知道 TypeScript 不理解這個文件類型,你的運行時可能不支持導入它。但是,如果已經配置了運行時或打包工具來處理它,則可以使用新--allowArbitraryExtensions
編譯器選項來抑制錯誤。
注意,可以通過添加一個名爲 app.css.d.ts
而不是 app.d.css.ts
的聲明文件通常可以實現類似的效果。然而,這只是通過 Node 對 CommonJS 的 require 解析規則實現的。嚴格來說,前者被解釋爲一個名爲 app.css.js
的 JavaScript 文件的聲明文件。 因爲相關文件導入需要在 Node 的 ESM 支持中包含擴展名,所以在我們的例子中,TypeScript 會在 --moduleResolution
node16 或 nodenext 下的 ESM 文件中出錯。
customConditions
--customConditions
獲取當 TypeScript 從 package.json
的 [exports] 或 ([nodejs.org/api/packages.html#exports]) 或 imports
字段解析時應該成功的附加的條件列表。這些條件將添加到解析器默認使用的現有條件中。
例如,當此字段在 tsconfig.json 中設置爲:
{
"compilerOptions": {
"target": "es2022",
"moduleResolution": "bundler",
"customConditions": ["my-condition"]
}
}
任何時候在 package.json 中引用 exports 或 imports 字段時,TypeScript 都會考慮名爲 my-condition 的條件。
因此,當從具有以下 package.json 的包中導入時:
{
// ...
"exports": {
".": {
"my-condition": "./foo.mjs",
"node": "./bar.mjs",
"import": "./baz.mjs",
"require": "./biz.mjs"
}
}
}
TypeScript 將嘗試查找與foo.mjs
對應的文件。這個字段只有在 node16、nodenext 和--modulerresolution
爲 bundler 時纔有效。
--verbatimModuleSyntax
默認情況下,TypeScript 會執行一些稱爲導入省略的操作。如果這樣寫:
import { Car } from "./car";
export function drive(car: Car) {
// ...
}
TypeScript 檢測到只對類型使用導入並完全刪除導入。輸出 JavaScript 可能是這樣的:
export function drive(car) {
// ...
}
大多數時候這很好,因爲如果 Car
不是從 ./car
導出的值,將得到一個運行時錯誤。但對於某些邊界情況,它確實增加了一層複雜性。例如,沒有像 import "./car"
這樣的語句,即完全放棄了 import
,這實際上對有無副作用的模塊產生影響。
TypeScript 的 JavaScript emit 策略也有另外幾層複雜性——省略導入並不總是由如何使用 import 驅動的,它通常還會參考值的聲明方式。所以並不總是很清楚是否像下面這樣的代碼:
export { Car } from "./car";
如果 Car 是用類之類的東西聲明的,那麼它可以保存在生成的 JavaScript 文件中。 但是,如果 Car 僅聲明爲類型別名或接口,則 JavaScript 文件不應導出 Car。
雖然 TypeScript 可能能夠根據來自跨文件的信息做出這些發出決策,但並非每個編譯器都可以。
imports 和 exports 的類型修飾符在這些情況下會有幫助。我們可以明確指定import
或export
僅用於類型分析,並且可以在 JavaScript 文件中使用類型修飾符完全刪除。
// 這條語句可以在JS輸出中完全刪除
import type * as car from "./car";
// 在JS輸出中可以刪除命名的import/export Car
import { type Car } from "./car";
export { type Car } from "./car";
類型修飾符本身並不是很有用——默認情況下,模塊省略仍然會刪除導入,並且沒有強制區分類型和普通導入和導出。 因此 TypeScript 有標誌 --importsNotUsedAsValues
以確保使用 type
修飾符,--preserveValueImports
以防止某些模塊省略行爲,以及 --isolatedModules
以確保 TypeScript 代碼適用於不同的編譯器。 不幸的是,很難理解這 3 個標誌的細節,並且仍然存在一些具有意外行爲的邊界情況。
TypeScript 5.0 引入了一個名爲 --verbatimModuleSyntax
的新選項來簡化這種情況。規則要簡單得多,任何沒有 type
修飾符的導入或導出都會被保留。任何使用 type
修飾符的內容都會被完全刪除。
// 完全被刪除
import type { A } from "a";
// 重寫爲 'import { b } from "bcd";'
import { b, type c, type d } from "bcd";
// 重寫爲 'import {} from "xyz";'
import { type xyz } from "xyz";
有了這個新選項,所見即所得。不過,當涉及到模塊互操作時,這確實有一些影響。 在此標誌下,當設置或文件擴展名暗示不同的模塊系統時,ECMAScript 導入和導出不會被重寫爲 require
調用。相反,會得到一個錯誤。 如果需要生成使用 require
和 module.exports
的代碼,則必須使用早於 ES2015 的 TypeScript 模塊語法: package.json
中設置 type
字段是很常見的。 因此,開發人員會在沒有意識到的情況下開始編寫 CommonJS 模塊而不是 ES 模塊,從而給出意外的查找規則和 JavaScript 輸出。 這個新標誌確保有意使用正在使用的文件類型,因爲語法是有意不同的。
因爲 --verbatimModuleSyntax
提供了比 --importsNotUsedAsValues
和 --preserveValueImports
更一致的作用,所以這兩個現有標誌被棄用了。
支持 export type *
當 TypeScript 3.8 引入僅類型導入時,新語法不允許在 export * from "module" 或 export * as ns from "module" 重新導出時使用。 TypeScript 5.0 添加了對這兩種形式的支持:
// models/vehicles.ts
export class Spaceship {
// ...
}
// models/index.ts
export type * as vehicles from "./vehicles";
// main.ts
import { vehicles } from "./models";
function takeASpaceship(s: vehicles.Spaceship) {
// ✅
}
function makeASpaceship() {
return new vehicles.Spaceship();
// ^^^^^^^^
// vehicles 不能用作值,因爲它是使用“export type”導出的。
}
JSDoc 支持 @satisfies
TypeScript 4.9 引入了 satisfies
操作符。它確保表達式的類型是兼容的,而不影響類型本身。以下面的代碼爲例:
interface CompilerOptions {
strict?: boolean;
outDir?: string;
// ...
}
interface ConfigSettings {
compilerOptions?: CompilerOptions;
extends?: string | string[];
// ...
}
let myConfigSettings = {
compilerOptions: {
strict: true,
outDir: "../lib",
// ...
},
extends: [
"@tsconfig/strictest/tsconfig.json",
"../../../tsconfig.base.json"
],
} satisfies ConfigSettings;
這裏,TypeScript 知道 myCompilerOptions.extends
是用數組聲明的,因爲雖然 satisfies
驗證了對象的類型,但它並沒有直接將其更改爲 CompilerOptions
而丟失信息。所以如果想映射到 extends
上,是可以的。
declare function resolveConfig(configPath: string): CompilerOptions;
let inheritedConfigs = myConfigSettings.extends.map(resolveConfig);
這對 TypeScript 用戶很有幫助,但是很多人使用 TypeScript 來使用 JSDoc 註釋對 JavaScript 代碼進行類型檢查。 這就是爲什麼 TypeScript 5.0 支持一個名爲 @satisfies
的新 JSDoc 標籤,它做的事情完全一樣。
/** @satisfies */
可以捕獲類型不匹配:
// @ts-check
/**
* @typedef CompilerOptions
* @prop {boolean} [strict]
* @prop {string} [outDir]
*/
/**
* @satisfies {CompilerOptions}
*/
let myCompilerOptions = {
outdir: "../lib",
// ~~~~~~ oops! we meant outDir
};
但它會保留表達式的原始類型,允許稍後在代碼中更精確地使用值。
// @ts-check
/**
* @typedef CompilerOptions
* @prop {boolean} [strict]
* @prop {string} [outDir]
*/
/**
* @typedef ConfigSettings
* @prop {CompilerOptions} [compilerOptions]
* @prop {string | string[]} [extends]
*/
/**
* @satisfies {ConfigSettings}
*/
let myConfigSettings = {
compilerOptions: {
strict: true,
outDir: "../lib",
},
extends: [
"@tsconfig/strictest/tsconfig.json",
"../../../tsconfig.base.json"
],
};
let inheritedConfigs = myConfigSettings.extends.map(resolveConfig);
/** @satisfies */
也可以內嵌在任何帶括號的表達式上。 可以這樣寫 myCompilerOptions
:
let myConfigSettings = /** @satisfies {ConfigSettings} */ ({
compilerOptions: {
strict: true,
outDir: "../lib",
},
extends: [
"@tsconfig/strictest/tsconfig.json",
"../../../tsconfig.base.json"
],
});
這可能在函數調用時更有意義:
compileCode(/** @satisfies {CompilerOptions} */ ({
// ...
}));
JSDoc 支持 @overload
在 TypeScript 中,可以爲函數指定重載。 重載提供了一種方式,用不同的參數調用一個函數,並返回不同的結果。它可以限制調用者實際使用函數的方式,並優化將返回的結果。
// 重載:
function printValue(str: string): void;
function printValue(num: number, maxFractionDigits?: number): void;
// 實現:
function printValue(value: string | number, maximumFractionDigits?: number) {
if (typeof value === "number") {
const formatter = Intl.NumberFormat("en-US", {
maximumFractionDigits,
});
value = formatter.format(value);
}
console.log(value);
}
這裏,printValue 將字符串或數字作爲第一個參數。如果它需要一個數字,它可以使用第二個參數來確定可以打印多少個小數位。
TypeScript 5.0 現在允許 JSDoc 使用新的 @overload
標籤聲明重載。 每個帶有 @overload
標籤的 JSDoc 註釋都被視爲以下函數聲明的不同重載。
// @ts-check
/**
* @overload
* @param {string} value
* @return {void}
*/
/**
* @overload
* @param {number} value
* @param {number} [maximumFractionDigits]
* @return {void}
*/
/**
* @param {string | number} value
* @param {number} [maximumFractionDigits]
*/
function printValue(value, maximumFractionDigits) {
if (typeof value === "number") {
const formatter = Intl.NumberFormat("en-US", {
maximumFractionDigits,
});
value = formatter.format(value);
}
console.log(value);
}
現在,無論是在 TypeScript 還是 JavaScript 文件中編寫,TypeScript 都可以讓我們知道是否錯誤地調用了函數。
printValue("hello!");
printValue(123.45);
printValue(123.45, 2);
printValue("hello!", 123); // ❌
編輯器中不區分大小寫的導入排序
在 Visual Studio 和 VS Code 等編輯器中,TypeScript 支持組織和排序導入和導出的體驗。 但是,對於列表何時 “排序”,通常會有不同的解釋。
例如,下面的導入列表是否排序?
import {
Toggle,
freeze,
toBoolean,
} from "./utils";
答案可能是 “視情況而定”。 如果不關心區分大小寫,那麼這個列表顯然沒有排序。 字母 f 出現在 t 和 T 之前。
但在大多數編程語言中,排序默認是比較字符串的字節值。JavaScript 比較字符串的方式意味着 “Toggle” 總是在 “freeze” 之前,因爲根據 ASCII 字符編碼,大寫字母在小寫字母之前。 所以從這個角度來看,導入列表是已排序的。
TypeScript 之前認爲導入列表是已排序的,因爲它會做基本的區分大小寫的排序。 對於喜歡不區分大小寫排序的開發人員,或者使用像 ESLint 這樣默認需要不區分大小寫排序的工具的開發人員來說,這可能是一個阻礙。
TypeScript 現在默認檢測大小寫。這意味着 TypeScript 和 ESLint 等工具通常不會就如何最好地對導入進行排序而相互 “鬥爭”。
這些選項最終可能由編輯器配置。目前,它們仍然不穩定且處於試驗階段,現在可以通過在 JSON 選項中使用 typescript.unstable
在 VS Code 中選擇加入它們。 以下是可以嘗試的所有選項(設置爲默認值):
{
"typescript.unstable": {
// Should sorting be case-sensitive? Can be:
// - true
// - false
// - "auto" (auto-detect)
"organizeImportsIgnoreCase": "auto",
// Should sorting be "ordinal" and use code points or consider Unicode rules? Can be:
// - "ordinal"
// - "unicode"
"organizeImportsCollation": "ordinal",
// Under `"organizeImportsCollation": "unicode"`,
// what is the current locale? Can be:
// - [any other locale code]
// - "auto" (use the editor's locale)
"organizeImportsLocale": "en",
// Under `"organizeImportsCollation": "unicode"`,
// should upper-case letters or lower-case letters come first? Can be:
// - false (locale-specific)
// - "upper"
// - "lower"
"organizeImportsCaseFirst": false,
// Under `"organizeImportsCollation": "unicode"`,
// do runs of numbers get compared numerically (i.e. "a1" < "a2" < "a100")? Can be:
// - true
// - false
"organizeImportsNumericCollation": true,
// Under `"organizeImportsCollation": "unicode"`,
// do letters with accent marks/diacritics get sorted distinctly
// from their "base" letter (i.e. is é different from e)? Can be
// - true
// - false
"organizeImportsAccentCollation": true
},
"javascript.unstable": {
// same options valid here...
},
}
完善 switch/case
在編寫 switch 語句時,TypeScript 現在會檢測被檢查的值何時具有字面量類型。以提供更便利的代碼快捷輸入:
速度、內存和包大小優化
TypeScript 5.0 在代碼結構、數據結構和算法實現中包含許多強大的變化。這些都意味着整個體驗應該更快——不僅僅是運行 TypeScript,甚至安裝它。
以下是相對於 TypeScript 4.9 在速度和大小方面的優勢:
| 場景 | 時間或大小相對於 TS 4.9 | | --- | --- | | material-ui 構建時間 | 90% | | TypeScript 編譯器啓動時間 | 89% | | Playwright 構建時間 | 88% | | TypeScript 編譯器自構建時間 | 87% | | Outlook Web 構建時間 | 82% | | VS Code 構建時間 | 80% | | TypeScript npm 包大小 | 59% |
圖表形式:
首先,將 TypeScript 從命名空間遷移到模塊,這樣就能夠利用現代構建工具來執行優化。重新審視了打包策略並刪除一些已棄用的代碼,已將 TypeScript 4.9 的 63.8 MB 包大小減少了約 26.4 MB。還通過直接函數調用帶來了顯著的速度提升。
在將信息序列化爲字符串時,執行了一些緩存。 類型顯示可能作爲錯誤報告、聲明觸發、代碼補全等的一部分發生,最終可能會相當昂貴。TypeScript 現在緩存了一些常用的機制以在這些操作中重用。
總的來說,預計大多數代碼庫應該會看到 TypeScript 5.0 的速度提升,並且始終能夠重現 10% 到 20% 之間的提升。當然,這將取決於硬件和代碼庫特性。
其他重大更改和棄用
運行時要求
TypeScript 現在的 target
是 ECMAScript 2018。TypeScript 軟件包還將預期的最低引擎版本設置爲 12.20。對於 Node.js 用戶來說,這意味着 TypeScript 5.0 需要至少 Node.js 12.20 或更高版本才能運行。
lib.d.ts 變化
更改 DOM 類型的生成方式可能會對現有代碼產生影響。注意,某些屬性已從數字轉換爲數字字面量類型,並且用於剪切、複製和粘貼事件處理的屬性和方法已跨接口移動。
API 重大變更
在 TypeScript 5.0 中, 轉向了模塊,刪除了一些不必要的接口,並進行了一些正確性改進。
關係運算符中的禁止隱式強制
如果編寫的代碼可能導致隱式字符串到數字的強制轉換,TypeScript 中的某些操作現在會進行警告:
function func(ns: number | string) {
return ns * 4; // 錯誤,可能存在隱式強制轉換
}
在 5.0 中,這也將應用於關係運算符 >、<、<= 和>=:
function func(ns: number | string) {
return ns > 4;
}
如果需要這樣做,可以使用+
顯式地將操作數轉換爲數字:
function func(ns: number | string) {
return +ns > 4; // OK
}
棄用和默認更改
在 TypeScript 5.0 中,棄用了以下設置和設置值:
-
--target: ES3
-
--out
-
--noImplicitUseStrict
-
--keyofStringsOnly
-
--suppressExcessPropertyErrors
-
--suppressImplicitAnyIndexErrors
-
--noStrictGenericChecks
-
--charset
-
--importsNotUsedAsValues
-
--preserveValueImports
在 TypeScript 5.5 之前,這些配置將繼續被允許使用,屆時它們將被完全刪除,但是,如果正在使用這些設置,將收到警告。 在 TypeScript 5.0 以及未來版本 5.1、5.2、5.3 和 5.4 中,可以指定 "ignoreDeprecations": "5.0"
以消除這些警告。 很快會發佈一個 4.9 補丁,允許指定 ignoreDeprecations
以實現更平滑的升級。除了棄用之外,還更改了一些設置以更好地改進 TypeScript 中的跨平臺行爲。
-
--newLine
,控制 JavaScript 文件中發出的行結束符,如果沒有指定,過去是根據當前操作系統推斷的。我們認爲構建應該儘可能確定,Windows 記事本現在支持換行符,所以新的默認設置是 LF。 舊的特定於操作系統的推理行爲不再可用。 -
--forceConsistentCasingInFileNames
,它確保項目中對相同文件名的所有引用都在大小寫中達成一致,現在默認爲true
。 這有助於捕獲在不區分大小寫的文件系統上編寫的代碼的差異問題。
參考資料
-
https://devblogs.microsoft.com/typescript/announcing-typescript-5-0-rc/
-
https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/venWqP_H9dMuDFwsawjg0A