TypeScript 5.0中的新特性: 宣告器、構造型別、列舉改進、速度以及更多內容

TypeScript 5.0中的新特性

TypeScript 5.0於2023年3月16日正式釋出,現在可以供大家使用了。這個版本引入了許多新功能,目的是使TypeScript更小、更簡單、更快速。

這個新版本對用於類定製的裝飾器進行了現代化處理,允許以可重複使用的方式定製類和其成員。開發人員現在可以在型別引數宣告中新增一個const修飾符,允許類似const的推斷成為預設的。新版本還使所有列舉成為union列舉,簡化了程式碼結構,加快了TypeScript的體驗。

在這篇文章中,你將探索TypeScript 5.0中引入的變化,深入瞭解其新特性和功能。

  1. 開始使用TypeScript 5.0
  2. TypeScript 5.0 有哪些新特性?
  3. 棄用

開始使用TypeScript 5.0

TypeScript 是一個官方編譯器,你可以使用 npm 安裝到你的專案中。如果你想在你的專案中開始使用TypeScript 5.0,你可以在你的專案目錄中執行以下命令:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
npm install -D typescript
npm install -D typescript
npm install -D typescript

這將在node_modules目錄下安裝編譯器,現在你可以用 npx tsc 命令執行它。

你還可以在這個文件中找到關於在Visual Studio Code中使用較新版本TypeScript的說明。

TypeScript 5.0 有哪些新特性?

在這篇文章中,讓我們來探討TypeScript中引入的5個主要更新。這些功能包括:

  1. 現代化的裝飾器
  2. 引入const型別引數
  3. 對列舉的改進
  4. TypeScript 5.0的效能改進
  5. 捆綁器決議,更好的模組決議

現代化的裝飾器

裝飾器在TypeScript中已經存在了一段時間,但新版本使其與ECMAScript的提議保持一致,現在處於第三階段,這意味著它處於被新增到TypeScript的階段。

裝飾器是一種以可重複使用的方式定製類和其成員的行為的方法。例如,如果你有一個類,它有兩個方法, greetgetAge

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
getAge() {
console.log(`I am ${this.age} years old.`);
}
}
const p = new Person('Ron', 30);
p.greet();
p.getAge();
class Person { name: string; age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } greet() { console.log(`Hello, my name is ${this.name}.`); } getAge() { console.log(`I am ${this.age} years old.`); } } const p = new Person('Ron', 30); p.greet(); p.getAge();
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
getAge() {
console.log(`I am ${this.age} years old.`);
}
}
const p = new Person('Ron', 30);
p.greet();
p.getAge();

在現實世界的用例中,這個類應該有更復雜的方法來處理一些非同步邏輯,並有副作用,例如,你會想扔進一些 console.log 呼叫來幫助除錯方法。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greet() {
console.log('LOG: Method Execution Starts.');
console.log(`Hello, my name is ${this.name}.`);
console.log('LOG: Method Execution Ends.');
}
getAge() {
console.log('LOG: Method Execution Starts.');
console.log(`I am ${this.age} years old.`);
console.log('Method Execution Ends.');
}
}
const p = new Person('Ron', 30);
p.greet();
p.getAge();
class Person { name: string; age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } greet() { console.log('LOG: Method Execution Starts.'); console.log(`Hello, my name is ${this.name}.`); console.log('LOG: Method Execution Ends.'); } getAge() { console.log('LOG: Method Execution Starts.'); console.log(`I am ${this.age} years old.`); console.log('Method Execution Ends.'); } } const p = new Person('Ron', 30); p.greet(); p.getAge();
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greet() {
console.log('LOG: Method Execution Starts.');
console.log(`Hello, my name is ${this.name}.`);
console.log('LOG: Method Execution Ends.');
}
getAge() {
console.log('LOG: Method Execution Starts.');
console.log(`I am ${this.age} years old.`);
console.log('Method Execution Ends.');
}
}
const p = new Person('Ron', 30);
p.greet();
p.getAge();

這是一個經常出現的模式,如果有一個適用於每一個方法的解決方案,那就很方便了。

這就是裝飾器發揮作用的地方。我們可以定義一個名為 debugMethod 的函式,如下所示:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function debugMethod(originalMethod: any, context: any) {
function replacementMethod(this: any, ...args: any[]) {
console.log('Method Execution Starts.');
const result = originalMethod.call(this, ...args);
console.log('Method Execution Ends.');
return result;
}
return replacementMethod;
}
function debugMethod(originalMethod: any, context: any) { function replacementMethod(this: any, ...args: any[]) { console.log('Method Execution Starts.'); const result = originalMethod.call(this, ...args); console.log('Method Execution Ends.'); return result; } return replacementMethod; }
function debugMethod(originalMethod: any, context: any) {
function replacementMethod(this: any, ...args: any[]) {
console.log('Method Execution Starts.');
const result = originalMethod.call(this, ...args);
console.log('Method Execution Ends.');
return result;
}
return replacementMethod;
}

在上面的程式碼中, debugMethod 接收原始方法( originalMethod ),並返回一個函式,做以下工作:

  1. 記錄 “Method Execution Starts.” 的資訊。
  2. 傳遞原始方法和它的所有引數(包括這個)。
  3. 記錄一條訊息 “Method Execution Ends.”。
  4. 返回原始方法返回的東西。

通過使用裝飾器,你可以將 debugMethod 應用到你的方法中,如下面的程式碼所示:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
@debugMethod
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
@debugMethod
getAge() {
console.log(`I am ${this.age} years old.`);
}
}
const p = new Person('Ron', 30);
p.greet();
p.getAge();
class Person { name: string; age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } @debugMethod greet() { console.log(`Hello, my name is ${this.name}.`); } @debugMethod getAge() { console.log(`I am ${this.age} years old.`); } } const p = new Person('Ron', 30); p.greet(); p.getAge();
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
@debugMethod
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
@debugMethod
getAge() {
console.log(`I am ${this.age} years old.`);
}
}
const p = new Person('Ron', 30);
p.greet();
p.getAge();

這將輸出以下內容:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
LOG: Entering method.
Hello, my name is Ron.
LOG: Exiting method.
LOG: Entering method.
I am 30 years old.
LOG: Exiting method.
LOG: Entering method. Hello, my name is Ron. LOG: Exiting method. LOG: Entering method. I am 30 years old. LOG: Exiting method.
LOG: Entering method.
Hello, my name is Ron.
LOG: Exiting method.
LOG: Entering method.
I am 30 years old.
LOG: Exiting method.

在定義裝飾函式( debugMethod )時,會傳遞第二個引數,叫做 context (它是context物件–有一些關於裝飾方法如何被宣告的有用資訊,也有方法的名稱)。你可以更新你的 debugMethod ,從 context 物件中獲取方法的名稱:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function debugMethod(
originalMethod: any,
context: ClassMethodDecoratorContext
) {
const methodName = String(context.name);
function replacementMethod(this: any, ...args: any[]) {
console.log(`'${methodName}' Execution Starts.`);
const result = originalMethod.call(this, ...args);
console.log(`'${methodName}' Execution Ends.`);
return result;
}
return replacementMethod;
}
function debugMethod( originalMethod: any, context: ClassMethodDecoratorContext ) { const methodName = String(context.name); function replacementMethod(this: any, ...args: any[]) { console.log(`'${methodName}' Execution Starts.`); const result = originalMethod.call(this, ...args); console.log(`'${methodName}' Execution Ends.`); return result; } return replacementMethod; }
function debugMethod(
originalMethod: any,
context: ClassMethodDecoratorContext
) {
const methodName = String(context.name);
function replacementMethod(this: any, ...args: any[]) {
console.log(`'${methodName}' Execution Starts.`);
const result = originalMethod.call(this, ...args);
console.log(`'${methodName}' Execution Ends.`);
return result;
}
return replacementMethod;
}

當你執行你的程式碼時,現在輸出將帶有每個用 debugMethod 裝飾器裝飾的方法的名字:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
'greet' Execution Starts.
Hello, my name is Ron.
'greet' Execution Ends.
'getAge' Execution Starts.
I am 30 years old.
'getAge' Execution Ends.
'greet' Execution Starts. Hello, my name is Ron. 'greet' Execution Ends. 'getAge' Execution Starts. I am 30 years old. 'getAge' Execution Ends.
'greet' Execution Starts.
Hello, my name is Ron.
'greet' Execution Ends.
'getAge' Execution Starts.
I am 30 years old.
'getAge' Execution Ends.

你可以用裝飾器做的事情還有很多。請隨時檢視原始拉取請求,以獲得更多關於如何在TypeScript中使用裝飾器的資訊。

引入const型別引數

這是另一個重要的版本,它為你提供了一個新的泛型工具,以改善你在呼叫函式時得到的推斷。預設情況下,當你用 const 宣告值時,TypeScript會推斷出型別而不是其字面值:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Inferred type: string[]
const names = ['John', 'Jake', 'Jack'];
// Inferred type: string[] const names = ['John', 'Jake', 'Jack'];
// Inferred type: string[]
const names = ['John', 'Jake', 'Jack'];

直到現在,為了實現所需的推理,你不得不通過新增 “as const “來使用const斷言:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Inferred type: readonly ["John", "Jake", "Jack"]
const names = ['John', 'Jake', 'Jack'] as const;
// Inferred type: readonly ["John", "Jake", "Jack"] const names = ['John', 'Jake', 'Jack'] as const;
// Inferred type: readonly ["John", "Jake", "Jack"]
const names = ['John', 'Jake', 'Jack'] as const;

當你呼叫函式時,情況類似。在下面的程式碼中,推斷出的countries的型別是 string[]

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
type HasCountries = { countries: readonly string[] };
function getCountriesExactly(arg: T): T['countries'] {
return arg.countries;
}
// Inferred type: string[]
const countries = getCountriesExactly({ countries: ['USA', 'Canada', 'India'] });
type HasCountries = { countries: readonly string[] }; function getCountriesExactly(arg: T): T['countries'] { return arg.countries; } // Inferred type: string[] const countries = getCountriesExactly({ countries: ['USA', 'Canada', 'India'] });
type HasCountries = { countries: readonly string[] };
function getCountriesExactly(arg: T): T['countries'] {
return arg.countries;
}
// Inferred type: string[]
const countries = getCountriesExactly({ countries: ['USA', 'Canada', 'India'] });

你可能希望有一個更具體的型別,在這之前,有一種方法可以解決這個問題,那就是新增 as const 斷言:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Inferred type: readonly ["USA", "Canada", "India"]
const names = getNamesExactly({ countries: ['USA', 'Canada', 'India'] } as const);
// Inferred type: readonly ["USA", "Canada", "India"] const names = getNamesExactly({ countries: ['USA', 'Canada', 'India'] } as const);
// Inferred type: readonly ["USA", "Canada", "India"]
const names = getNamesExactly({ countries: ['USA', 'Canada', 'India'] } as const);

這可能很難記住和實現。然而,TypeScript 5.0引入了一個新的功能,你可以在型別引數宣告中新增一個const修飾符,這將自動應用一個類似const的預設推理。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
type HasCountries = { countries: readonly string[] };
function getNamesExactly(arg: T): T['countries'] {
return arg.countries;
}
// Inferred type: readonly ["USA", "Canada", "India"]
const names = getNamesExactly({ countries: ['USA', 'Canada', 'India'] });
type HasCountries = { countries: readonly string[] }; function getNamesExactly(arg: T): T['countries'] { return arg.countries; } // Inferred type: readonly ["USA", "Canada", "India"] const names = getNamesExactly({ countries: ['USA', 'Canada', 'India'] });
type HasCountries = { countries: readonly string[] };
function getNamesExactly(arg: T): T['countries'] {
return arg.countries;
}
// Inferred type: readonly ["USA", "Canada", "India"]
const names = getNamesExactly({ countries: ['USA', 'Canada', 'India'] });

使用 const 型別引數可以讓開發者在他們的程式碼中更清楚地表達意圖。如果一個變數打算成為常量,並且永遠不會改變,那麼使用 const 型別引數可以確保它永遠不會被意外地改變。

你可以檢視原始拉取請求,瞭解更多關於const型別引數在TypeScript中的工作原理。

對列舉的改進

TypeScript 中的列舉是一個強大的結構,允許開發者定義一組命名的常量。在TypeScript 5.0中,對列舉進行了改進,使其更加靈活和有用。

例如,如果你有以下列舉傳入一個函式:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
enum Color {
Red,
Green,
Blue,
}
function getColorName(colorLevel: Color) {
return colorLevel;
}
console.log(getColorName(1));
enum Color { Red, Green, Blue, } function getColorName(colorLevel: Color) { return colorLevel; } console.log(getColorName(1));
enum Color {
Red,
Green,
Blue,
}
function getColorName(colorLevel: Color) {
return colorLevel;
}
console.log(getColorName(1));

在引入TypeScript 5.0之前,你可以傳遞一個錯誤的級別號,而且它不會丟擲錯誤。但隨著TypeScript 5.0的引入,它將立即丟擲一個錯誤。

此外,新版本通過為每個計算成員建立一個獨特的型別,使所有列舉成為聯合列舉。這一改進允許縮小所有列舉的範圍,並將其成員作為型別進行引用:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
enum Color {
Red,
Purple,
Orange,
Green,
Blue,
Black,
White,
}
type PrimaryColor = Color.Red | Color.Green | Color.Blue;
function isPrimaryColor(c: Color): c is PrimaryColor {
return c === Color.Red || c === Color.Green || c === Color.Blue;
}
console.log(isPrimaryColor(Color.White)); // Outputs: false
console.log(isPrimaryColor(Color.Red)); // Outputs: true
enum Color { Red, Purple, Orange, Green, Blue, Black, White, } type PrimaryColor = Color.Red | Color.Green | Color.Blue; function isPrimaryColor(c: Color): c is PrimaryColor { return c === Color.Red || c === Color.Green || c === Color.Blue; } console.log(isPrimaryColor(Color.White)); // Outputs: false console.log(isPrimaryColor(Color.Red)); // Outputs: true
enum Color {
Red,
Purple,
Orange,
Green,
Blue,
Black,
White,
}
type PrimaryColor = Color.Red | Color.Green | Color.Blue;
function isPrimaryColor(c: Color): c is PrimaryColor {
return c === Color.Red || c === Color.Green || c === Color.Blue;
}
console.log(isPrimaryColor(Color.White)); // Outputs: false
console.log(isPrimaryColor(Color.Red)); // Outputs: true

TypeScript 5.0的效能改進

TypeScript 5.0 包括程式碼結構、資料結構和演算法擴充套件方面的眾多重大變化。這有助於改善整個 TypeScript 體驗,從安裝到執行,使其更快、更高效。

例如,TypeScript 5.0和4.9的包大小之間的差異是相當驚人的。

TypeScript最近從名稱空間遷移到了模組,使其能夠利用現代構建工具,可以進行範圍提升等優化。此外,刪除了一些廢棄的程式碼,從TypeScript 4.9的63.8 MB包大小中減少了約26.4 MB。

TypeScript包的大小

TypeScript包的大小

下面是TypeScript 5.0和4.9之間在速度和大小上的一些更有趣的對比:

場景 相對於TS 4.9的時間或大小
material-ui構建時間 90%
TypeScript編譯器的啟動時間 89%
Playwright構建時間 88%
TypeScript Compiler自建時間 87%
Outlook Web構建時間 82%
VS Code構建時間 80%
typescript npm包大小 59%

捆綁器解決更好的模組解析

當你在TypeScript中寫一個匯入語句時,編譯器需要知道這個匯入指的是什麼。它使用模組解析來實現這一點。例如,當你寫 import { a } from "moduleA" 時,編譯器需要知道 moduleAa 的定義以檢查其使用。

在TypeScript 4.7中,為 --module 和 moduleResolution 設定增加了兩個新選項: node16nodenext

這些選項的目的是為了更準確地表示Node.js中ECMAScript模組的精確查詢規則。然而,這種模式有幾個限制,是其他工具所不能執行的。

例如,在Node.js中的ECMAScript模組中,任何相對匯入都必須包括一個副檔名才能正確工作:

import * as utils from "./utils"; // Wrong 
import * as utils from "./utils.mjs"; // Correct

TypeScript引入了一個新的策略,叫做 “moduleResolution bundler”。這個策略可以通過在你的TypeScript配置檔案的 “compilerOptions “部分新增以下程式碼來實現:

{
"compilerOptions": {
"target": "esnext",
"moduleResolution": "bundler"
}
}

這個新策略適用於那些使用現代捆綁器的人,如Vite、esbuild、swc、Webpack、Parcel和其他利用混合查詢策略的捆綁器。

你可以檢視原始拉取請求及其實現,以瞭解更多關於 moduleResolution 捆綁器在TypeScript中如何工作的資訊。

變動和棄用

TypeScript 5.0新增了部分變動和棄用,包括執行時間要求、lib.d.ts 更改和 API 突破性更改。

  1. 執行時要求: TypeScript現在以ECMAScript 2018為目標,該包設定的最低引擎期望值為12.20。因此,Node.js的使用者應該至少有12.20或更高的版本來使用TypeScript 5.0。
  2. lib.d.ts的變化: 對於如何生成DOM的型別有一些變化,這可能會影響到現有的程式碼。特別是,某些屬性已經從數字轉換為數字字面型別,用於剪下、複製和貼上事件處理的屬性和方法已經跨介面移動。
  3. API的突破性變化: 一些不必要的介面已被刪除,並進行了一些正確性的改進。TypeScript 5.0也已經轉移到了模組。

TypeScript 5.0已經廢棄了某些設定和它們相應的值,包括目標: target: ES3outnoImplicitUseStrictkeyofStringsOnlysuppressExcessPropertyErrorssuppressImplicitAnyIndexErrorsnoStrictGenericCheckscharsetimportsNotUsedAsValues, 和 preserveValueImports,以及專案引用中的prepend。

雖然這些配置在TypeScript 5.5之前仍然有效,但我們會發出警告,提醒仍在使用這些配置的使用者。

小結

在這篇文章中,你已經瞭解了TypeScript 5.0帶來的一些主要功能和改進,比如對列舉、捆綁器解析和常量型別引數的改進,以及對速度和大小的改進。

評論留言