TypeScript入門 一、什麼是TypeScript JavaScript的超集,可以編譯成JavaScript。添加了類型系統的JavaScript,可以適用於任何規模的項目。 TypeScript特性 類型系統 從 TypeScript 的名字就可以看出來,「類型」是其最核心的特性。 我 ...
TypeScript入門
一、什麼是TypeScript
JavaScript的超集,可以編譯成JavaScript。添加了類型系統的JavaScript,可以適用於任何規模的項目。
TypeScript特性
類型系統
從 TypeScript 的名字就可以看出來,「類型」是其最核心的特性。
我們知道,JavaScript 是一門非常靈活的編程語言:
- 它沒有類型約束,一個變數可能初始化時是字元串,過一會兒又被賦值為數字。
- 由於隱式類型轉換的存在,有的變數的類型很難在運行前就確定。
- 基於原型的面向對象編程,使得原型上的屬性或方法可以在運行時被修改。
- 函數是 JavaScript 中的一等公民,可以賦值給變數,也可以當作參數或返回值。
這種靈活性就像一把雙刃劍,一方面使得 JavaScript 蓬勃發展,無所不能,從 2013 年開始就一直蟬聯最普遍使用的編程語言排行榜冠軍[3];另一方面也使得它的代碼質量參差不齊,維護成本高,運行時錯誤多。
而 TypeScript 的類型系統,在很大程度上彌補了 JavaScript 的缺點。
TypeScript 是靜態類型
類型系統按照「類型檢查的時機」來分類,可以分為動態類型和靜態類型。
動態類型是指在運行時才會進行類型檢查,這種語言的類型錯誤往往會導致運行時錯誤。JavaScript 是一門解釋型語言,沒有編譯階段,所以它是動態類型,以下這段代碼在運行時才會報錯:
let foo = 1;
foo.split(' ');
// Uncaught TypeError: foo.split is not a function
// 運行時會報錯(foo.split 不是一個函數),造成線上 bug
靜態類型是指編譯階段就能確定每個變數的類型,這種語言的類型錯誤往往會導致語法錯誤。TypeScript 在運行前需要先編譯為 JavaScript,而在編譯階段就會進行類型檢查,所以 TypeScript 是靜態類型,這段 TypeScript 代碼在編譯階段就會報錯了:
let foo = 1;
foo.split(' ');
// Property 'split' does not exist on type 'number'.
// 編譯時會報錯(數字沒有 split 方法),無法通過編譯
你可能會奇怪,這段 TypeScript 代碼看上去和 JavaScript 沒有什麼區別呀。
沒錯!大部分 JavaScript 代碼都只需要經過少量的修改(或者完全不用修改)就變成 TypeScript 代碼,這得益於 TypeScript 強大的[類型推論][],即使不去手動聲明變數 foo
的類型,也能在變數初始化時自動推論出它是一個 number
類型。
完整的 TypeScript 代碼是這樣的:
let foo: number = 1;
foo.split(' ');
// Property 'split' does not exist on type 'number'.
// 編譯時會報錯(數字沒有 split 方法),無法通過編譯
TypeScript 是弱類型
類型系統按照「是否允許隱式類型轉換」來分類,可以分為強類型和弱類型。
以下這段代碼不管是在 JavaScript 中還是在 TypeScript 中都是可以正常運行的,運行時數字 1
會被隱式類型轉換為字元串 '1'
,加號 +
被識別為字元串拼接,所以列印出結果是字元串 '11'
。
console.log(1 + '1');
// 列印出字元串 '11'
TypeScript 是完全相容 JavaScript 的,它不會修改 JavaScript 運行時的特性,所以它們都是弱類型。
二、安裝並編譯TypeScript
安裝TypeSscript需要nodejs環境,如果電腦沒有npm命令的,可以去官網下載並安裝nodejs
TypeScript安裝命令
npm install -g typescript
//通過tsc --version 可以檢查版本號確保全裝成功
安裝以後編譯ts文件很簡單,我們在電腦上新建一個目錄,code,新建一個文件index.ts
然後在當前目錄下輸入命令:
tsc index.ts
編譯完成後會在當面目錄下輸出一個index.js文件,編譯成功。
我們有時候想指定目錄進行輸出:
tsc --outFile ./js/index.js index.ts
三、基本數據類型
布爾值
布爾值是最基礎的數據類型,在 TypeScript 中,使用 boolean
定義布爾值類型:
let isDone: boolean = false;
數值
使用 number
定義數值類型:
let num : number = 1;
字元串
使用 string
定義字元串類型:
let myName: string = 'Tom';
// 模板字元串
let sentence: string = `Hello, my name is ${myName}.`;
空值
JavaScript 沒有空值(Void)的概念,在 TypeScript 中,可以用 void
表示沒有任何返回值的函數:
function alertName(): void {
alert('My name is Tom');
}
聲明一個 void
類型的變數沒有什麼用,因為你只能將它賦值為 undefined
和 null
let unusable: void = undefined;
Null 和 Undefined
在 TypeScript 中,可以使用 null
和 undefined
來定義這兩個原始數據類型:
let u: undefined = undefined;
let n: null = null;
四、任意值(Any)
任意值(Any)用來表示允許賦值為任意類型。
如果是一個普通類型,在賦值過程中改變類型是不被允許的:
let myFavoriteNumber: string = 'seven';
myFavoriteNumber = 7;
// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.
但如果是 any
類型,則允許被賦值為任意類型。
let myFavoriteNumber: any = 'seven';
myFavoriteNumber = 7;
在任意值上訪問任何屬性都是允許的:
let anyThing: any = 'hello';
console.log(anyThing.myName);
console.log(anyThing.myName.firstName);
也允許調用任何方法:
let anyThing: any = 'Tom';
anyThing.setName('Jerry');
anyThing.setName('Jerry').sayHello();
anyThing.myName.setFirstName('Cat');
所以,聲明一個變數為任意值之後,對它的任何操作,返回的內容的類型都是任意值。
五、類型推論
如果沒有明確的指定類型,那麼 TypeScript 會依照類型推論(Type Inference)的規則推斷出一個類型。
以下代碼雖然沒有指定類型,但是會在編譯的時候報錯:
let myFavoriteNumber = 'seven';
myFavoriteNumber = 7;
// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.
事實上,它等價於:
let myFavoriteNumber: string = 'seven';
myFavoriteNumber = 7;
// index.ts(2,1): error TS2322: Type 'number' is not assignable to type 'string'.
TypeScript 會在沒有明確的指定類型的時候推測出一個類型,這就是類型推論。
如果定義的時候沒有賦值,不管之後有沒有賦值,都會被推斷成 any
類型而完全不被類型檢查:
let myFavoriteNumber;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;
六、聯合類型
聯合類型(Union Types)表示取值可以為多種類型中的一種。
舉個慄子
let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;
聯合類型使用 |
分隔每個類型。
這裡的 let myFavoriteNumber: string | number
的含義是,允許 myFavoriteNumber
的類型是 string
或者 number
,但是不能是其他類型。
比如下麵這個慄子就會報錯:
let myFavoriteNumber: string | number;
myFavoriteNumber = true;
// index.ts(2,1): error TS2322: Type 'boolean' is not assignable to type 'string | number'.
// Type 'boolean' is not assignable to type 'number'.
訪問聯合類型的屬性或方法
當 TypeScript 不確定一個聯合類型的變數到底是哪個類型的時候,我們只能訪問此聯合類型的所有類型里共有的屬性或方法:
function getLength(something: string | number): number {
return something.length;
}
// index.ts(2,22): error TS2339: Property 'length' does not exist on type 'string | number'.
// Property 'length' does not exist on type 'number'.
上例中,length
不是 string
和 number
的共有屬性,所以會報錯。
訪問 string
和 number
的共有屬性是沒問題的:
function getString(something: string | number): string {
return something.toString();
}
聯合類型的變數在被賦值的時候,會根據類型推論的規則推斷出一個類型:
let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
console.log(myFavoriteNumber.length); // 5
myFavoriteNumber = 7;
console.log(myFavoriteNumber.length); // 編譯時報錯
// index.ts(5,30): error TS2339: Property 'length' does not exist on type 'number'.
上例中,第二行的 myFavoriteNumber
被推斷成了 string
,訪問它的 length
屬性不會報錯。
而第四行的 myFavoriteNumber
被推斷成了 number
,訪問它的 length
屬性時就報錯了。
七、介面
在 TypeScript 中,我們使用Interfaces來定義一個介面類型的對象。
什麼是介面
在面向對象語言中,介面(Interfaces)是一個很重要的概念,它是對行為的抽象,而具體如何行動需要由類去實現。
TypeScript的核心原則之一是對值所具有的結構進行類型檢查。 它有時被稱做“鴨式辨型法”或“結構性子類型化”。 在TypeScript里,介面的作用就是為這些類型命名和為你的代碼或第三方代碼定義契約。
舉個例子
interface Person {
name: string;
age: number;
}
let tom: Person = {
name: 'Tom',
age: 25
};
上面的例子中,我們定義了一個介面 Person,接著定義了一個變數 tom,它的類型是 Person。這樣,我們就約束了 tom 的形狀必須和介面 Person 一致。
介面一般首字母大寫。有的編程語言中會建議介面的名稱加上 I 首碼。
定義的變數比介面少了一些屬性是不允許的:
interface Person {
name: string;
age: number;
}
let tom: Person = {
name: 'Tom'
};
// index.ts(6,5): error TS2322: Type '{ name: string; }' is not assignable to type 'Person'.
// Property 'age' is missing in type '{ name: string; }'.
多一些屬性也是不允許的:
interface Person {
name: string;
age: number;
}
let tom: Person = {
name: 'Tom',
age: 25,
gender: 'male'
};
// index.ts(9,5): error TS2322: Type '{ name: string; age: number; gender: string; }' is not assignable to type 'Person'.
// Object literal may only specify known properties, and 'gender' does not exist in type 'Person'.
可見,賦值的時候,變數的形狀必須和介面的形狀保持一致。
可選屬性
有時我們希望不要完全匹配一個形狀,那麼可以用可選屬性:
interface Person {
name: string;
age?: number;
}
let tom: Person = {
name: 'Tom'
};
interface Person {
name: string;
age?: number;
}
let tom: Person = {
name: 'Tom',
age: 25
};
任意屬性
有時候我們希望一個介面允許有任意的屬性,可以使用如下方式:
interface Person {
name: string;
age?: number;
[propName: string]: any;
}
let tom: Person = {
name: 'Tom',
gender: 'male'
};
使用 [propName: string]
定義了任意屬性取 string
類型的值。
只讀屬性
有時候我們希望對象中的一些欄位只能在創建的時候被賦值,那麼可以用 readonly
定義只讀屬性:
interface Person {
readonly id: number;
name: string;
age?: number;
[propName: string]: any;
}
let tom: Person = {
id: 89757,
name: 'Tom',
gender: 'male'
};
tom.id = 9527;
// index.ts(14,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.
上例中,使用 readonly
定義的屬性 id
初始化後,又被賦值了,所以報錯了。
八、數組
存放多個元素的集合
最簡單的方法是使用「類型 + 方括弧」來表示數組:
let fibonacci: number[] = [1, 1, 2, 3, 5];
數組的項中不允許出現其他的類型:
let fibonacci: number[] = [1, '1', 2, 3, 5];
// Type 'string' is not assignable to type 'number'.
數組的一些方法的參數也會根據數組在定義時約定的類型進行限制:
let fibonacci: number[] = [1, 1, 2, 3, 5];
fibonacci.push('8');
// Argument of type '"8"' is not assignable to parameter of type 'number'.
上例中,push
方法只允許傳入 number
類型的參數,但是卻傳了一個 "8"
類型的參數,所以報錯了。這裡 "8"
是一個字元串字面量類型,會在後續章節中詳細介紹。
也可以指定一個any類型的數組:
let list: any[] = ['xcatliu', 25, { website: 'http://xcatliu.com' }];
九、函數01,函數聲明、函數表達式
函數聲明
在 JavaScript 中,有兩種常見的定義函數的方式——函數聲明(Function Declaration)和函數表達式(Function Expression):
// 函數聲明(Function Declaration)
function sum(x, y) {
return x + y;
}
// 函數表達式(Function Expression)
let mySum = function (x, y) {
return x + y;
};
一個函數有輸入和輸出,要在 TypeScript 中對其進行約束,需要把輸入和輸出都考慮到,其中函數聲明的類型定義較簡單:
function sum(x: number, y: number): number {
return x + y;
}
註意,輸入多餘的(或者少於要求的)參數,是不被允許的:
function sum(x: number, y: number): number {
return x + y;
}
sum(1, 2, 3);
// index.ts(4,1): error TS2346: Supplied parameters do not match any signature of call target.
function sum(x: number, y: number): number {
return x + y;
}
sum(1);
// index.ts(4,1): error TS2346: Supplied parameters do not match any signature of call target.
函數表達式
如果要我們現在寫一個對函數表達式(Function Expression)的定義,可能會寫成這樣:
let mySum = function (x: number, y: number): number {
return x + y;
};
這是可以通過編譯的,不過事實上,上面的代碼只對等號右側的匿名函數進行了類型定義,而等號左邊的 mySum
,是通過賦值操作進行類型推論而推斷出來的。如果需要我們手動給 mySum
添加類型,則應該是這樣:
let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
return x + y;
};
註意不要混淆了 TypeScript 中的 =>
和 ES6 中的 =>
。
在 TypeScript 的類型定義中,=>
用來表示函數的定義,左邊是輸入類型,需要用括弧括起來,右邊是輸出類型。
在 ES6 中,=>
叫做箭頭函數,應用十分廣泛,可以參考ES6 中的箭頭函數
用介面定義函數的形狀
我們也可以使用介面的方式來定義一個函數需要符合的形狀:
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
return source.search(subString) !== -1;
}
採用函數表達式|介面定義函數的方式時,對等號左側進行類型限制,可以保證以後對函數名賦值時保證參數個數、參數類型、返回值類型不變。
十、函數02,可選參數,參數預設值
可選參數
前面提到,輸入多餘的(或者少於要求的)參數,是不允許的。那麼如何定義可選的參數呢?
與介面中的可選屬性類似,我們用 ?
表示可選的參數:
function buildName(firstName: string, lastName?: string) {
if (lastName) {
return firstName + ' ' + lastName;
} else {
return firstName;
}
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');
需要註意的是,可選參數必須接在必需參數後面。換句話說,可選參數後面不允許再出現必需參數了:
function buildName(firstName?: string, lastName: string) {
if (firstName) {
return firstName + ' ' + lastName;
} else {
return lastName;
}
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName(undefined, 'Tom');
// index.ts(1,40): error TS1016: A required parameter cannot follow an optional parameter.
參數預設值
在 ES6 中,我們允許給函數的參數添加預設值,TypeScript 會將添加了預設值的參數識別為可選參數:
function buildName(firstName: string, lastName: string = 'Cat') {
return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let tom = buildName('Tom');
此時就不受「可選參數必須接在必需參數後面」的限制了:
function buildName(firstName: string = 'Tom', lastName: string) {
return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let cat = buildName(undefined, 'Cat');
十一、函數03,剩餘參數、重載
ES6 中,可以使用 ...rest
的方式獲取函數中的剩餘參數(rest 參數):
function push(array, ...items) {
items.forEach(function(item) {
array.push(item);
});
}
let a: any[] = [];
push(a, 1, 2, 3);
事實上,items
是一個數組。所以我們可以用數組的類型來定義它:
function push(array: any[], ...items: any[]) {
items.forEach(function(item) {
array.push(item);
});
}
let a = [];
push(a, 1, 2, 3);
註意,rest 參數只能是最後一個參數,關於 rest 參數,可以參考 ES6 中的 rest 參數。
重載
重載允許一個函數接受不同數量或類型的參數時,作出不同的處理。方法名相同,參數列表和類型不同。
比如,我們需要實現一個函數 reverse
,輸入數字 123
的時候,輸出反轉的數字 321
,輸入字元串 'hello'
的時候,輸出反轉的字元串 'olleh'
。
利用聯合類型,我們可以這麼實現:
function reverse(x: number | string): number | string | void {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else if (typeof x === 'string') {
return x.split('').reverse().join('');
}
}
然而這樣有一個缺點,就是不能夠精確的表達,輸入為數字的時候,輸出也應該為數字,輸入為字元串的時候,輸出也應該為字元串。
這時,我們可以使用重載定義多個 reverse
的函數類型:
function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string | void {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else if (typeof x === 'string') {
return x.split('').reverse().join('');
}
}
上例中,我們重覆定義了多次函數 reverse
,前幾次都是函數定義,最後一次是函數實現。在編輯器的代碼提示中,可以正確的看到前兩個提示。
註意,TypeScript 會優先從最前面的函數定義開始匹配,所以多個函數定義如果有包含關係,需要優先把精確的定義寫在前面。
十二、類型斷言01,語法,將一個聯合類型斷言為其中一個類型
類型斷言(Type Assertion)可以用來手動指定一個值的類型。
語法
值 as 類型
或者
<類型>值
在 tsx 語法(React 的 jsx 語法的 ts 版)中必須使用前者,即 值 as 類型
。
故建議大家在使用類型斷言時,統一使用 值 as 類型
這樣的語法。
類型斷言有多重用途
將一個聯合類型斷言為其中一個類型
之前提到過,當 TypeScript 不確定一個聯合類型的變數到底是哪個類型的時候,我們只能訪問此聯合類型的所有類型中共有的屬性或方法:
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function getName(animal: Cat | Fish) {
return animal.name;
}
而有時候,我們確實需要在還不確定類型的時候就訪問其中一個類型特有的屬性或方法,比如:
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function isFish(animal: Cat | Fish) {
if (typeof animal.swim === 'function') {
return true;
}
return false;
}
// index.ts:11:23 - error TS2339: Property 'swim' does not exist on type 'Cat | Fish'.
// Property 'swim' does not exist on type 'Cat'.
上面的例子中,獲取 animal.swim
的時候會報錯。
此時可以使用類型斷言,將 animal
斷言成 Fish
:
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function isFish(animal: Cat | Fish) {
if (typeof (animal as Fish).swim === 'function') {
return true;
}
return false;
}
這樣就可以解決訪問 animal.swim
時報錯的問題了。
需要註意的是,類型斷言只能夠「欺騙」TypeScript 編譯器,無法避免運行時的錯誤,反而濫用類型斷言可能會導致運行時錯誤:
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function swim(animal: Cat | Fish) {
(animal as Fish).swim();
}
const tom: Cat = {
name: 'Tom',
run() { console.log('run') }
};
swim(tom);
// Uncaught TypeError: animal.swim is not a function`
上面的例子編譯時不會報錯,但在運行時會報錯:
Uncaught TypeError: animal.swim is not a function`
原因是 (animal as Fish).swim()
這段代碼隱藏了 animal
可能為 Cat
的情況,將 animal
直接斷言為 Fish
了,而 TypeScript 編譯器信任了我們的斷言,故在調用 swim()
時沒有編譯錯誤。
可是 swim
函數接受的參數是 Cat | Fish
,一旦傳入的參數是 Cat
類型的變數,由於 Cat
上沒有 swim
方法,就會導致運行時錯誤了。
總之,使用類型斷言時一定要格外小心,儘量避免斷言後調用方法或引用深層屬性,以減少不必要的運行時錯誤。
十三、類型斷言02,將一個父類斷言為更加具體的子類
當類之間有繼承關係時,類型斷言也是很常見的:
class ApiError extends Error {
code: number = 0;
}
class HttpError extends Error {
statusCode: number = 200;
}
function isApiError(error: Error) {
if (typeof (error as ApiError).code === 'number') {
return true;
}
return false;
}
上面的例子中,我們聲明瞭函數 isApiError,它用來判斷傳入的參數是不是 ApiError 類型,為了實現這樣一個函數,它的參數的類型肯定得是比較抽象的父類 Error,這樣的話這個函數就能接受 Error 或它的子類作為參數了。
但是由於父類 Error 中沒有 code 屬性,故直接獲取 error.code 會報錯,需要使用類型斷言獲取 (error as ApiError).code。
大家可能會註意到,在這個例子中有一個更合適的方式來判斷是不是 ApiError,那就是使用 instanceof:
class ApiError extends Error {
code: number = 0;
}
class HttpError extends Error {
statusCode: number = 200;
}
function isApiError(error: Error) {
if (error instanceof ApiError) {
return true;
}
return false;
}
上面的例子中,確實使用 instanceof 更加合適,因為 ApiError 是一個 JavaScript 的類,能夠通過 instanceof 來判斷 error 是否是它的實例。
但是有的情況下 ApiError 和 HttpError 不是一個真正的類,而只是一個 TypeScript 的介面(interface),介面是一個類型,不是一個真正的值,它在編譯結果中會被刪除,當然就無法使用 instanceof 來做運行時判斷了:
interface ApiError extends Error {
code: number;
}
interface HttpError extends Error {
statusCode: number;
}
function isApiError(error: Error) {
if (error instanceof ApiError) {
return true;
}
return false;
}
// index.ts:9:26 - error TS2693: 'ApiError' only refers to a type, but is being used as a value here.
此時就只能用類型斷言,通過判斷是否存在 code
屬性,來判斷傳入的參數是不是 ApiError
了:
interface ApiError extends Error {
code: number;
}
interface HttpError extends Error {
statusCode: number;
}
function isApiError(error: Error) {
if (typeof (error as ApiError).code === 'number') {
return true;
}
return false;
}
十四、類型斷言03,將任何一個類型斷言為 any
理想情況下,TypeScript 的類型系統運轉良好,每個值的類型都具體而精確。
當我們引用一個在此類型上不存在的屬性或方法時,就會報錯:
const foo: number = 1;
foo.length = 1;
// index.ts:2:5 - error TS2339: Property 'length' does not exist on type 'number'.
上面的例子中,數字類型的變數 foo
上是沒有 length
屬性的,故 TypeScript 給出了相應的錯誤提示。
這種錯誤提示顯然是非常有用的。
但有的時候,我們非常確定這段代碼不會出錯,比如下麵這個例子:
window.foo = 1;
// index.ts:1:8 - error TS2339: Property 'foo' does not exist on type 'Window & typeof globalThis'.
上面的例子中,我們需要將 window 上添加一個屬性 foo,但 TypeScript 編譯時會報錯,提示我們 window 上不存在 foo 屬性。
此時我們可以使用 as any 臨時將 window 斷言為 any 類型:
(window as any).foo = 1;
在 any 類型的變數上,訪問任何屬性都是允許的。
需要註意的是,將一個變數斷言為 any 可以說是解決 TypeScript 中類型問題的最後一個手段。
它極有可能掩蓋了真正的類型錯誤,所以如果不是非常確定,就不要使用 as any。
總之,一方面不能濫用 as any,另一方面也不要完全否定它的作用,我們需要在類型的嚴格性和開發的便利性之間掌握平衡(這也是 TypeScript 的設計理念之一),才能發揮出 TypeScript 最大的價值。
十五、類型斷言04,將 any 斷言為一個具體的類型
在日常的開發中,我們不可避免的需要處理 any 類型的變數,它們可能是由於第三方庫未能定義好自己的類型,也有可能是歷史遺留的或其他人編寫的爛代碼,還可能是受到 TypeScript 類型系統的限制而無法精確定義類型的場景。
遇到 any 類型的變數時,我們可以選擇無視它,任由它滋生更多的 any。
我們也可以選擇改進它,通過類型斷言及時的把 any 斷言為精確的類型,亡羊補牢,使我們的代碼向著高可維護性的目標發展。
舉例來說,歷史遺留的代碼中有個 getCacheData,它的返回值是 any:
function getCacheData(key: string): any {
return (window as any).cache[key];
}
那麼我們在使用它時,最好能夠將調用了它之後的返回值斷言成一個精確的類型,這樣就方便了後續的操作:
function getCacheData(key: string): any {
return (window as any).cache[key];
}
interface Cat {
name: string;
run(): void;
}
const tom = getCacheData('tom') as Cat;
tom.run();
上面的例子中,我們調用完 getCacheData 之後,立即將它斷言為 Cat 類型。這樣的話明確了 tom 的類型,後續對 tom 的訪問時就有了代碼補全,提高了代碼的可維護性。
十六、類型斷言05,類型斷言的限制
從上面的例子中,我們可以總結出:
- 聯合類型可以被斷言為其中一個類型
- 父類可以被斷言為子類
- 任何類型都可以被斷言為 any
- any 可以被斷言為任何類型
那麼類型斷言有沒有什麼限制呢?是不是任何一個類型都可以被斷言為任何另一個類型呢?
答案是否定的——並不是任何一個類型都可以被斷言為任何另一個類型。
具體來說,若 A
相容 B
,那麼 A
能夠被斷言為 B
,B
也能被斷言為 A
。
下麵我們通過一個簡化的例子,來理解類型斷言的限制:
interface Animal {
name: string;
}
interface Cat {
name: string;
run(): void;
}
function testAnimal(animal: Animal) {
return (animal as Cat);
}
function testCat(cat: Cat) {
return (cat as Animal);
}
上面的慄子中是可以斷言的,我們再看看下麵的慄子:
interface Animal {
name: string;
}
interface Cat {
run(): void;
}
function testAnimal(animal: Animal) {
return (animal as Cat);
}
function testCat(cat: Cat) {
return (cat as Animal);
}
這個時候會提示錯誤,兩者不能充分重疊,這意味要想斷言成功, 還必須具備一個條件:
- 要使得
A
能夠被斷言為B
,只需要A
相容B
或B
相容A
即可
十七、類型斷言06,雙重斷言
既然:
- 任何類型都可以被斷言為 any
- any 可以被斷言為任何類型
那麼我們是不是可以使用雙重斷言 as any as Foo
來將任何一個類型斷言為任何另一個類型呢?
interface Cat {
run(): void;
}
interface Fish {
swim(): void;
}
function testCat(cat: Cat) {
return (cat as any as Fish);
}
在上面的例子中,若直接使用 cat as Fish
肯定會報錯,因為 Cat
和 Fish
互相都不相容。
但是若使用雙重斷言,則可以打破「要使得 A
能夠被斷言為 B
,只需要 A
相容 B
或 B
相容 A
即可」的限制,將任何一個類型斷言為任何另一個類型。
若你使用了這種雙重斷言,那麼十有八九是非常錯誤的,它很可能會導致運行時錯誤。
除非迫不得已,千萬別用雙重斷言。
十八、類型斷言07,類型斷言 vs 類型轉換
類型斷言只會影響 TypeScript 編譯時的類型,類型斷言語句在編譯結果中會被刪除:
function toBoolean(something: any): boolean {
return something as boolean;
}
toBoolean(1);
// 返回值為 1
在上面的例子中,將 something
斷言為 boolean
雖然可以通過編譯,但是並沒有什麼用,代碼在編譯後會變成:
function toBoolean(something) {
return something;
}
toBoolean(1);
// 返回值為 1
所以類型斷言不是類型轉換,它不會真的影響到變數的類型。
若要進行類型轉換,需要直接調用類型轉換的方法:
function toBoolean(something: any): boolean {
return Boolean(something);
}
toBoolean(1);
// 返回值為 true
十九、類型斷言08,類型斷言 vs 類型聲明
在這個例子中:
function getCacheData(key: string): any {
return (window as any).cache[key];
}
interface Cat {
name: string;
run(): void;
}
const tom = getCacheData('tom') as Cat;
tom.run();
我們使用 as Cat
將 any
類型斷言為了 Cat
類型。
但實際上還有其他方式可以解決這個問題:
function getCacheData(key: string): any {
return (window as any).cache[key];
}
interface Cat {
name: string;
run(): void;
}
const tom: Cat = getCacheData('tom');
tom.run();
上面的例子中,我們通過類型聲明的方式,將 tom
聲明為 Cat
,然後再將 any
類型的 getCacheData('tom')
賦值給 Cat
類型的 tom
。
這和類型斷言是非常相似的,而且產生的結果也幾乎是一樣的——tom
在接下來的代碼中都變成了 Cat
類型。
它們的區別,可以通過這個例子來理解:
interface Animal {
name: string;
}
interface Cat {
name: string;
run(): void;
}
const animal: Animal = {
name: 'tom'
};
let tom = animal as Cat;
在上面的例子中,由於 Animal
相容 Cat
,故可以將 animal
斷言為 Cat
賦值給 tom
。
但是若直接聲明 tom
為 Cat
類型:
interface Animal {
name: string;
}
interface Cat {
name: string;
run(): void;
}
const animal: Animal = {
name: 'tom'
};
let tom: Cat = animal;
// index.ts:12:5 - error TS2741: Property 'run' is missing in type 'Animal' but required in type 'Cat'.
則會報錯,不允許將 animal
賦值為 Cat
類型的 tom
。
我們可以得出結論:
//a斷言為b時,a和b有重疊的部分即可
//a聲明為b時,a必須具備b的所有屬性和方法
知道了它們的核心區別,就知道了類型聲明是比類型斷言更加嚴格的。
所以為了增加代碼的質量,我們最好優先使用類型聲明,這也比類型斷言的 as
語法更加優雅。
二十、類型斷言09,類型斷言 vs 泛型
還是這個例子:
function getCacheData(key: string): any {
return (window as any).cache[key];
}
interface Cat {
name: string;
run(): void;
}
const tom = getCacheData('tom') as Cat;
tom.run();
我們還有第三種方式可以解決這個問題,那就是泛型:
function getCacheData<T>(key: string): T {
return (window as any).cache[key];
}
interface Cat {
name: string;
run(): void;
}
const tom = getCacheData<Cat>('tom');
tom.run();
通過給 getCacheData
函數添加了一個泛型 <T>
,我們可以更加規範的實現對 getCacheData
返回值的約束,這也同時去除掉了代碼中的 any
,是最優的一個解決方案。
二一、使用type關鍵字定義類型別名和字元串字面量類型
我們來看一個方法:
function getName(n: string| (() => string)): string {
if (typeof n === 'string') {
return n;
} else {
return n();
}
}
類型別名用來給一個類型起個新名字
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
if (typeof n === 'string') {
return n;
} else {
return n();
}
}
上例中,我們使用 type
創建類型別名。
類型別名常用於聯合類型。
字元串字面量類型用來約束取值只能是某幾個字元串中的一個
type EventNames = 'click' | 'scroll' | 'mousemove';
function handleEvent(ele: Element, event: EventNames) {
// do something
}
handleEvent(document.getElementById('hello'), 'scroll'); // 沒問題
handleEvent(document.getElementById('world'), 'dblclick'); // 報錯,event 不能為 'dblclick'
上例中,我們使用 type
定了一個字元串字面量類型 EventNames
,它只能取三種字元串中的一種。
註意,類型別名與字元串字面量類型都是使用 type
進行定義。
二二、元組
數組合併了相同類型的對象,而元組(Tuple)合併了不同類型的對象。
元組起源於函數編程語言(如 F#),這些語言中會頻繁使用元組。
舉個例子
定義一對值分別為 string
和 number
的元組:
let tom: [string, number] = ['Tom', 25];
當賦值或訪問一個已知索引的元素時,會得到正確的類型:
let tom: [string, number];
tom[0] = 'Tom';
tom[1] = 25;
也可以只賦值其中一項:
let tom: [string, number];
tom[0] = 'Tom';
但是當直接對元組類型的變數進行初始化或者賦值的時候,需要提供所有元組類型中指定的項。
let tom: [string, number];
tom = ['Tom', 25];
下麵這樣就不行:
let tom: [string, number];
tom = ['Tom'];
// Property '1' is missing in type '[string]' but required in type '[string, number]'.
越界的元素
當添加越界的元素時,它的類型會被限製為元組中每個類型的聯合類型:
let tom: [string, number];
tom = ['Tom', 25];
tom.push('male');//可以添加string
tom.push(true);//但不能添加boolean
// Argument of type 'true' is not assignable to parameter of type 'string | number'.
二三、枚舉
枚舉(Enum)類型用於取值被限定在一定範圍內的場景,比如一周只能有七天,顏色限定為紅綠藍等。
枚舉使用 enum
關鍵字來定義:
enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};
枚舉成員會被賦值為從 0
開始遞增的數字,同時也會對枚舉值到枚舉名進行反向映射:
enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};
console.log(Days["Sun"] === 0); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true
console.log(Days[0] === "Sun"); // true
console.log(Days[1] === "Mon"); // true
console.log(Days[2] === "Tue"); // true
console.log(Days[6] === "Sat"); // true
事實上,上面的例子會被編譯為:
var Days;
(function (Days) {
Days[Days["Sun"] = 0] = "Sun";
Days[Days["Mon"] = 1] = "Mon";
Days[Days["Tue"] = 2] = "Tue";
Days[Days["Wed"] = 3] = "Wed";
Days[Days["Thu"] = 4] = "Thu";
Days[Days["Fri"] = 5] = "Fri";
Days[Days["Sat"] = 6] = "Sat";
})(Days || (Days = {}));
二四、類01,概念、構造函數、屬性和方法
類的概念
雖然 JavaScript 中有類的概念,但是可能大多數 JavaScript 程式員並不是非常熟悉類,這裡對類相關的概念做一個簡單的介紹。
- 類(Class):定義了一件事物的抽象特點,包含它的屬性和方法
- 對象(Object):類的實例,通過
new
生成 - 面向對象(OOP)的三大特性:封裝、繼承、多態
- 封裝(Encapsulation):將對數據的操作細節隱藏起來,只暴露對外的介面。外界調用端不需要(也不可能)知道細節,就能通過對外提供的介面來訪問該對象,同時也保證了外界無法任意更改對象內部的數據
- 繼承(Inheritance):子類繼承父類,子類除了擁有父類的所有特性外,還有一些更具體的特性
- 多態(Polymorphism):由繼承而產生了相關的不同的類,對同一個方法可以有不同的響應。比如
Cat
和Dog
都繼承自Animal
,但是分別實現了自己的eat
方法。此時針對某一個實例,我們無需瞭解它是Cat
還是Dog
,就可以直接調用eat
方法,程式會自動判斷出來應該如何執行eat
- 存取器(getter & setter):用以改變屬性的讀取和賦值行為
- 修飾符(Modifiers):修飾符是一些關鍵字,用於限定成員或類型的性質。比如
public
表示公有屬性或方法 - 抽象類(Abstract Class):抽象類是供其他類繼承的基類,抽象類不允許被實例化。抽象類中的抽象方法必須在子類中被實現
- 介面(Interfaces):不同類之間公有的屬性或方法,可以抽象成一個介面。介面可以被類實現(implements)。一個類只能繼承自另一個類,但是可以實現多個介面
使用 class
定義類,使用 constructor
定義構造函數。
通過 new
生成新實例的時候,會自動調用構造函數。
class Animal {
public _name;
constructor(name:string) {
this._name = name;
}
sayHi() {
return `My name is ${this._name}`;
}
}
let a = new Animal('Jack');
console.log(a.sayHi()); // My name is Jack
二五、類02,存取器:get,set
使用 getter 和 setter 可以改變屬性的賦值和讀取行為:
class Animal {
constructor(name) {
this.name = name;
}
get name() {
return 'Jack';
}
set name(value) {
console.log('setter: ' + value);
}
}
let a = new Animal('Kitty'); // setter: Kitty
a.name = 'Tom'; // setter: Tom
console.log(a.name); // Jack
二六、類03,靜態方法
使用 static
修飾符修飾的方法稱為靜態方法,它們不需要實例化,而是直接通過類來調用:
class Animal {
public _name;
constructor(name:string) {
this._name = name;
}
sayHi() {
return `My name is ${this._name}`;
}
static sayHello(){
return "hello,你好呀"
}
}
let a = new Animal('Jack');
console.log(a.sayHi()); // My name is Jack
console.log(Animal.sayHello()); // hello,你好呀
二七、類04,三種訪問修飾符:public、private 和 protected
-
public:全局的,公共的,當前類所涉及到的地方都可以使用
class Animal { public _name; public constructor(name:string) { this._name = name; } } let a = new Animal('Jack'); console.log(a._name); // Jack a._name = 'Tom'; console.log(a._name); // Tom
-
private:私有的,只能在類的內部使用,無法在實例化後通過類的實例.屬性來訪問
class Animal { private _name; public constructor(name:string) { this._name = name; } } let a = new Animal('Jack'); console.log(a._name); // 報錯 a._name = 'Tom'; // 報錯 console.log(a._name); // 報錯
-
protected:受保護的,private不允許子類訪問,使用protected就可以了
class Animal { protected name; public constructor(name:string) { this.name = name; } } class Cat extends Animal { constructor(name:string) { super(name); console.log(this.name); } }
二八、類05,參數屬性和只讀屬性關鍵字
修飾符和readonly
還可以使用在構造函數參數中,等同於類中定義該屬性同時給該屬性賦值,使代碼更簡潔。
class Animal {
// public name: string;
public constructor(public name: string) {
// this.name = name;
}
}
只讀屬性
class Animal {
readonly name;
public constructor(name:string) {
this.name = name;
}
}
let a = new Animal('Jack');
console.log(a.name); // Jack
a.name = 'Tom';
// index.ts(10,3): TS2540: Cannot assign to 'name' because it is a read-only property.
二九、類06,抽象類
abstract
用於定義抽象類和其中的抽象方法。
什麼是抽象類?
首先,抽象類是不允許被實例化的:
abstract class Animal {
public name;
public constructor(name:string) {
this.name = name;
}
public abstract sayHi():void;
}
let a = new Animal('Jack');
// index.ts(9,11): error TS2511: Cannot create an instance of the abstract class 'Animal'.
上面的例子中,我們定義了一個抽象類 Animal
,並且定義了一個抽象方法 sayHi
。在實例化抽象類的時候報錯了。
其次,抽象類中的抽象方法必須被子類實現:
abstract class Animal {
public name;
public constructor(name: string) {
this.name = name;
}
public abstract sayHi(): void;
}
class Cat extends Animal {
public eat() {
console.log(`${this.name} is eating.`);
}
}
let cat = new Cat('Tom');
// index.ts(9,7): error TS2515: Non-abstract class 'Cat' does not implement inherited abstract member 'sayHi' from class 'Animal'.
上面的例子中,我們定義了一個類 Cat
繼承了抽象類 Animal
,但是沒有實現抽象方法 sayHi
,所以編譯報錯了。
下麵是一個正確使用抽象類的例子:
abstract class Animal {
public name;
public constructor(name) {
this.name = name;
}
public abstract sayHi();
}
class Cat extends Animal {
public sayHi() {
console.log(`Meow, My name is ${this.name}`);
}
}
let cat = new Cat('Tom');
三十、類與介面,類繼承介面,介面繼承介面,介面繼承類
類繼承介面
實現(implements)是面向對象中的一個重要概念。一般來講,一個類只能繼承自另一個類,有時候不同類之間可以有一些共有的特性,這時候就可以把特性提取成介面(interfaces),用 implements
關鍵字來實現。這個特性大大提高了面向對象的靈活性。
舉例來說,門是一個類,防盜門是門的子類。如果防盜門有一個報警器的功能,我們可以簡單的給防盜門添加一個報警方法。這時候如果有另一個類,車,也有報警器的功能,就可以考慮把報警器提取出來,作為一個介面,防盜門和車都去實現它:
interface Alarm {
alert(): void;
}
class Door {
}
class SecurityDoor extends Door implements Alarm {
alert() {
console.log('SecurityDoor alert');
}
}
class Car implements Alarm {
alert() {
console.log('Car alert');
}
}
一個類可以實現多個介面:
interface Alarm {
alert(): void;
}
interface Light {
lightOn(): void;
lightOff(): void;
}
class Car implements Alarm, Light {
alert() {
console.log('Car alert');
}
lightOn() {
console.log('Car light on');
}
lightOff() {
console.log('Car light off');
}
}
上例中,Car
實現了 Alarm
和 Light
介面,既能報警,也能開關車燈。
介面繼承介面
介面與介面之間可以是繼承關係:
interface Alarm {
alert(): void;
}
interface LightableAlarm extends Alarm {
lightOn(): void;
lightOff(): void;
}
這很好理解,LightableAlarm
繼承了 Alarm
,除了擁有 alert
方法之外,還擁有兩個新方法 lightOn
和 lightOff
。
介面繼承類
常見的面向對象語言中,介面是不能繼承類的,但是在 TypeScript 中卻是可以的:
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
interface Point3d extends Point {
z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};
但是這裡不推薦這樣去使用,我們在定義介面的時候只做定義,具體實現通過繼承介面的類去實現。養成好習慣。
三一、泛型01,概念,簡單示例
泛型(Generics)是指在定義函數、介面或類的時候,不預先指定具體的類型,而在使用的時候再指定類型的一種特性。
首先,我們來實現一個函數 createArray
,它可以創建一個指定長度的數組,同時將每一項都填充一個預設值:
function createArray(length: number, value: any): Array<any> {
let result = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray(3, 'x'); // ['x', 'x', 'x']
上例中,我們使用了之前提到過的數組泛型來定義返回值的類型。
這段代碼編譯不會報錯,但是一個顯而易見的缺陷是,它並沒有準確的定義返回值的類型:
Array
這時候,泛型就派上用場了:
function createArray<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray<string>(3, 'x'); // ['x', 'x', 'x']
上例中,我們在函數名後添加了 <T>
,其中 T
用來指代任意輸入的類型,在後面的輸入 value: T
和輸出 Array<T>
中即可使用了。
接著在調用的時候,可以指定它具體的類型為 string
。當然,也可以不手動指定,而讓類型推論自動推算出來:
function createArray<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray(3, 'x'); // ['x', 'x', 'x']
三二、泛型02,多個類型參數
定義泛型的時候,可以一次定義多個類型參數:
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]];
}
swap([7, 'seven']); // ['seven', 7]
上例中,我們定義了一個 swap
函數,用來交換輸入的元組。
三三、泛型03,泛型約束
在函數內部使用泛型變數的時候,由於事先不知道它是哪種類型,所以不能隨意的操作它的屬性或方法:
function loggingIdentity<T>(arg: T): T {
console.log(arg.length);
return arg;
}
// index.ts(2,19): error TS2339: Property 'length' does not exist on type 'T'.
上例中,泛型 T
不一定包含屬性 length
,所以編譯的時候報錯了。
這時,我們可以對泛型進行約束,只允許這個函數傳入那些包含 length
屬性的變數。這就是泛型約束:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
上例中,我們使用了 extends
約束了泛型 T
必須符合介面 Lengthwise
的形狀,也就是必須包含 length
屬性。
此時如果調用 loggingIdentity
的時候,傳入的 arg
不包含 length
,那麼在編譯階段就會報錯了:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
loggingIdentity(7);
// index.ts(10,17): error TS2345: Argument of type '7' is not assignable to parameter of type 'Lengthwise'.
三四、泛型04,泛型介面
之前學習過,可以使用介面的方式來定義一個函數需要符合的形狀:
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
return source.search(subString) !== -1;
}
當然也可以使用含有泛型的介面來定義函數的形狀:
interface CreateArrayFunc {
<T>(length: number, value: T): Array<T>;
}
let createArray: CreateArrayFunc;
createArray = function<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray(3, 'x'); // ['x', 'x', 'x']
進一步,我們可以把泛型參數提前到介面名上:
interface CreateArrayFunc<T> {
(length: number, value: T): Array<T>;
}
let createArray: CreateArrayFunc<any>;
createArray = function<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray(3, 'x'); // ['x', 'x', 'x']
註意,此時在使用泛型介面的時候,需要定義泛型的類型。
三五、泛型05,泛型類
與泛型介面類似,泛型也可以用於類的類型定義中:
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
泛型參數的預設類型
在 TypeScript 2.3 以後,我們可以為泛型中的類型參數指定預設類型。當使用泛型時沒有在代碼中直接指定類型參數,從實際值參數中也無法推測出時,這個預設類型就會起作用。
function createArray<T = string>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
三六、聲明合併,同名函數、介面、類的合併
如果定義了兩個相同名字的函數、介面或類,那麼它們會合併成一個類型:
函數的合併
之前學習過,我們可以使用重載定義多個函數類型:
function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else if (typeof x === 'string') {
return x.split('').reverse().join('');
}
}
介面的合併
介面中的屬性在合併時會簡單的合併到一個介面中:
interface Alarm {
price: number;
}
interface Alarm {
weight: number;
}
相當於:
interface Alarm {
price: number;
weight: number;
}
註意,合併的屬性的類型必須是唯一的:
interface Alarm {
price: number;
}
interface Alarm {
price: number; // 雖然重覆了,但是類型都是 `number`,所以不會報錯
weight: number;
}
interface Alarm {
price: number;
}
interface Alarm {
price: string; // 類型不一致,會報錯
weight: number;
}
// index.ts(5,3): error TS2403: Subsequent variable declarations must have the same type. Variable 'price' must be of type 'number', but here has type 'string'.
介面中方法的合併,與函數的合併一樣:
interface Alarm {
price: number;
alert(s: string): string;
}
interface Alarm {
weight: number;
alert(s: string, n: number): string;
}
相當於:
interface Alarm {
price: number;
weight: number;
alert(s: string): string;
alert(s: string, n: number): string;
}
類的合併
類的合併與介面的合併規則一致。
PS:但是一般情況下,不建議創建多個同名介面或者類,雖然可以自動合併,但是可能會發現意想不到的問題。代碼不要寫在兩個地方,不然不好維護。
三七、寫在結尾
TypeScript的應用非常廣泛,最新的Vue和React均集成了TypeScript,這裡推薦大家使用Vue3,Vue3天然支持TS。
另外一方面,TypeScript中有很多的ES語法,這裡用一張圖來示意兩者之間的關係:
所以對ES5或者ES6不瞭解的同學,可以學習ES,瞭解一下相關的語法,這對於學習TypeScript也有一定的幫助。
在學習TS的過程中,也推薦大家多看官方文檔,官網的文檔比較詳細和豐富:
https://www.tslang.cn/docs/home.html