TypeScrip 基礎與進階概念_Part 2

可選屬性

有時我們希望不要完全匹配一個形狀,那麼可以用可選屬性 (?):

1
2
3
4
5
6
7
8
interface Person {
name: string;
age?: number;
}

let tom: Person = {
name: 'Tom'
};

仍然不允許新增未定義的屬性

任意屬性

有時候我們希望一個介面允許有任意的屬性,可以使用如下方式:

使用 [propName: string] 定義了任意屬性取 string 型別的值。

1
2
3
4
5
6
7
8
9
10
interface Person {
name: string;
age?: number;
[propName: string]: any;
}

let tom: Person = {
name: 'Tom',
gender: 'male'
};

需要注意的是,一旦定義了任意屬性,那麼確定屬性和可選屬性的型別都必須是它的型別的子集 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Person {
name: string;
age?: number;
[propName: string]: string;
}

let tom: Person = {
name: 'Tom',
age: 25,
gender: 'male'
};

// index.ts(3,5): error TS2411: Property 'age' of type 'number' is not assignable to string index type 'string'.
// index.ts(7,5): error TS2322: Type '{ [x: string]: string | number; name: string; age: number; gender: string; }' is not assignable to type 'Person'.
// Index signatures are incompatible.
// Type 'string | number' is not assignable to type 'string'.
// Type 'number' is not assignable to type 'string'.

簡單來說,可選屬性的型別要跟任意屬性的型別一樣

唯讀屬性

有時候我們希望物件中的一些欄位只能在建立的時候被賦值,那麼可以用 readonly 定義唯讀屬性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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.

------------------

// 對 tom 進行賦值的時候
// 1. 沒有給 id 賦值,會報錯
// 2. 在給 tom.id 賦值的時候,由於它是唯讀屬性,所以報錯了
let tony: Person = {
name: 'Tony',
gender: 'male'
};

tony.id = 89757;

// index.ts(8,5): error TS2322: Type '{ name: string; gender: string; }' is not assignable to type 'Person'.
// Property 'id' is missing in type '{ name: string; gender: string; }'.
// index.ts(13,5): error TS2540: Cannot assign to 'id' because it is a constant or a read-only property.

注意:唯讀的約束存在於第一次給「物件」賦值的時候,而不是第一次給「唯讀屬性」賦值的時候

陣列的型別

最簡單的方法是使用「型別 + 方括號」來表示陣列:

1
let fibonacci: number[] = [1, 1, 2, 3, 5];

陣列的項中不允許出現其他的型別:

1
2
3
let fibonacci: number[] = [1, '1', 2, 3, 5];

// Type 'string' is not assignable to type 'number'.

陣列的一些方法的引數也會根據陣列在定義時約定的型別進行限制:

1
2
3
4
let fibonacci: number[] = [1, 1, 2, 3, 5];
fibonacci.push('8');

// Argument of type '"8"' is not assignable to parameter of type 'number'.
  • 陣列泛型

我們也可以使用陣列泛型(Array Generic) Array<elemType> 來表示陣列:

1
let fibonacci: Array<number> = [1, 1, 2, 3, 5];
  • 用介面表示陣列

1
2
3
4
5
// NumberArray 表示:只要索引的型別是數字時,那麼值的型別必須是數字。
interface NumberArray {
[index: number]: number;
}
let fibonacci: NumberArray = [1, 1, 2, 3, 5];

雖然介面也可以用來描述陣列,但是我們一般不會這麼做,因為這種方式比前兩種方式複雜多了。

不過有一種情況例外,那就是它常用來表示類別陣列。

  • 類別陣列

類別陣列(Array-like Object)不是陣列型別,比如 arguments:

1
2
3
4
5
function sum() {
let args: number[] = arguments;
}

// Type 'IArguments' is missing the following properties from type 'number[]': pop, push, concat, join, and 24 more.

上例中,arguments 實際上是一個類別陣列,不能用普通的陣列的方式來描述,而應該用介面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function sum() {
let args: {
[index: number]: number;
length: number;
callee: Function;
} = arguments;
}

------------

// IArguments 是 TypeScript 中定義好了的型別
interface IArguments {
[index: number]: any;
length: number;
callee: Function;
}

在這個例子中,我們除了約束當索引的型別是數字時,值的型別必須是數字之外,也約束了它還有 length 和 callee 兩個屬性

事實上常用的類別陣列都有自己的介面定義,如 IArguments, NodeList, HTMLCollection 等:

1
2
3
function sum() {
let args: IArguments = arguments;
}
  • any 在陣列中的應用

1
let list: any[] = ['xcatliu', 25, { website: 'http://xcatliu.com' }];

函式的型別

  • 函式宣告(Function Declaration)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 函式宣告(Function Declaration)
function sum(x, y) {
return x + y;
}

// 函式表示式(Function Expression)
let mySum = function (x, y) {
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.

sum(1);

// index.ts(4,1): error TS2346: Supplied parameters do not match any signature of call target.

注意,輸入多餘的(或者少於要求的)引數,是不被允許的

  • 函式表示式(Function Expression)

1
2
3
4
5
6
7
8
9
10
11
12
// 只對等號右側的匿名函式進行了型別定義,
// 而等號左邊的 mySum,是透過賦值操作進行型別推論而推斷出來的
let mySum = function (x: number, y: number): number {
return x + y;
};

---------------------

// 如果需要我們手動給 mySum 新增型別,則應該是這樣
let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
return x + y;
};

注意不要混淆了 TypeScript 中的 => 和 ES6 中的 =>

  • TypeScript 中的 =>
    用來表示函數型別,描述函數的參數和返回值
  • ES6 中的 =>
    用來表示箭頭函數,是一種簡潔的函數寫法,並且不會改變 this 的指向。

  • 用介面定義函式的形狀

1
2
3
4
5
6
7
8
interface SearchFunc {
(source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
return source.search(subString) !== -1;
}
  • 可選引數

1
2
3
4
5
6
7
8
9
10
11
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.

可選引數後面不允許再出現必需引數了

  • 引數預設值

不受「可選引數必須接在必需引數後面」的限制

1
2
3
4
5
function buildName(firstName: string = 'Tom', lastName: string) {
return firstName + ' ' + lastName;
}
let tomcat = buildName('Tom', 'Cat');
let cat = buildName(undefined, 'Cat');
  • 剩餘引數

ES6 中,可以使用 …rest 的方式獲取函式中的剩餘引數(rest 引數):

1
2
3
4
5
6
7
8
9
// Javascript
function push(array, ...items) {
items.forEach(function(item) {
array.push(item);
});
}

let a = [];
push(a, 1, 2, 3);

事實上,items 是一個數組。所以我們可以用陣列的型別來定義它:

1
2
3
4
5
6
7
8
9
// Typescript
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’。

利用聯合型別,我們可以這麼實現:

1
2
3
4
5
6
7
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('');
}
}

然而這樣有一個缺點,就是不能夠精確的表達,輸入為數字的時候,輸出也應該為數字,輸入為字串的時候,輸出也應該為字串。

這時,我們可以使用過載定義多個 reverse 的函式型別:

1
2
3
4
5
6
7
8
9
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('');
}
}

上例中,我們重複定義了多次函式 reverse,前幾次都是函式定義,最後一次是函式實現。在編輯器的程式碼提示中,可以正確的看到前兩個提示。

注意
TypeScript 會優先從最前面的函式定義開始匹配,所以多個函式定義如果有包含關係,需要優先把精確的定義寫在前面

宣告檔案

TypeScript 新手指南 - 宣告檔案

內建物件

TypeScript 新手指南 - 內建物件

進階

型別別名

型別別名用來給一個型別起個新名字。

型別別名常用於聯合型別。

1
2
3
4
5
6
7
8
9
10
11
// 使用 type 建立型別別名
ype Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
if (typeof n === 'string') {
return n;
} else {
return n();
}
}

字串字面量型別

字串字面量 (String Literal) 型別用來約束取值只能是某幾個字串中的一個。

使用 type 定了一個字串字面量型別 EventNames,它只能取三種字串中的一種 :

1
2
3
4
5
6
7
8
9
type EventNames = 'click' | 'scroll' | 'mousemove';
function handleEvent(ele: Element, event: EventNames) {
// do something
}

handleEvent(document.getElementById('hello'), 'scroll'); // 沒問題
handleEvent(document.getElementById('world'), 'dbclick'); // 報錯,event 不能為 'dbclick'

// index.ts(7,47): error TS2345: Argument of type '"dbclick"' is not assignable to parameter of type 'EventNames'.

注意,型別別名與字串字面量型別都是使用 type 進行定義。

元組

數組合併了相同型別的物件,而元組(Tuple)合併了不同型別的物件。

  • 元組是一種特殊的陣列,它的元素型別和順序是固定的。
  • 陣列通常是相同型別的元素集合,且數量和順序可以變動。
1
let tom: [string, number] = ['Tom', 25];

當賦值或訪問一個已知索引的元素時,會得到正確的型別:

1
2
3
4
5
6
let tom: [string, number];
tom[0] = 'Tom';
tom[1] = 25;

tom[0].slice(1);
tom[1].toFixed(2);

也可以只賦值其中一項:

1
2
let tom: [string, number];
tom[0] = 'Tom';

但是當直接對元組型別的變數進行初始化或者賦值的時候,需要提供所有元組型別中指定的項。

1
2
3
4
5
6
7
8
9
10
11
// 正確
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]'.

越界的元素

如果要在元組要多新增元素時,新增元素的型別會被限制為元組中的每個型別的聯合型別。

1
2
3
4
5
6
7
8
let tom: [string, number];
tom = ['Tom', 25];
// 正確
tom.push('male');
// 錯誤
tom.push(true);

// Argument of type 'true' is not assignable to parameter of type 'string | number'.

枚舉 / 列舉 (Enum)

用於取值被限定在一定範圍內的場景,比如一週只能有七天,顏色限定為紅綠藍等。

列舉使用 enum 關鍵字來定義:

1
enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat};

列舉成員會被賦值為從 0 開始遞增的數字,同時也會對列舉值到列舉名進行反向對映:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 = {}));

手動賦值

1
2
3
4
5
6
7
enum Days {Sun = 7, Mon = 1, Tue, Wed, Thu, Fri, Sat};

// 未手動賦值的列舉項會接著上一個列舉項遞增。
console.log(Days["Sun"] === 7); // true
console.log(Days["Mon"] === 1); // true
console.log(Days["Tue"] === 2); // true
console.log(Days["Sat"] === 6); // true

重複賦值 :loop:

如果未手動賦值的列舉項與手動賦值的重複了,TypeScript 是不會察覺到這一點的

手動賦值的列舉項也可以為小數負數,此時後續未手動賦值的項的遞增步長仍為 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
enum Days {Sun = 3, Mon = 1, Tue, Wed, Thu, Fri, Sat};

console.log(Days["Sun"] === 3); // true
console.log(Days["Wed"] === 3); // true
console.log(Days[3] === "Sun"); // false
console.log(Days[3] === "Wed"); // true

--------------------

// 上面的例子中,遞增到 3 的時候與前面的 Sun 的取值重複了,
// 但是 TypeScript 並沒有報錯,導致 Days[3] 的值先是 "Sun",
// 而後又被 "Wed" 覆蓋了。編譯的結果是:
var Days;
(function (Days) {
Days[Days["Sun"] = 3] = "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 = {}));

使用的時候需要注意,最好不要出現這種覆蓋的情況。

常數項(constant member)和計算所得項(computed member)

計算所得項的例子:

1
2
3
4
5
6
7
8
9
10
// 正確
enum Color {Red, Green, Blue = "blue".length};

----------------

// 錯誤
enum Color {Red = "red".length, Green, Blue};

// index.ts(1,33): error TS1061: Enum member must have initializer.
// index.ts(1,40): error TS1061: Enum member must have initializer.

如果緊接在計算所得項後面的是未手動賦值的項,那麼它就會因為無法獲得初始值而報錯

當滿足以下條件時,列舉成員被當作是常數

  • 不具有初始化函式並且之前的列舉成員是常數。在這種情況下,當前列舉成員的值為上一個列舉成員的值加 1。但第一個列舉元素是個例外。如果它沒有初始化方法,那麼它的初始值為 0。

  • 列舉成員使用常數列舉表示式初始化。常數列舉表示式是 TypeScript 表示式的子集,它可以在編譯階段求值。當一個表示式滿足下面條件之一時,它就是一個常數列舉表示式:

    • 數字字面量:簡單的數字,比如 5。
    • 參考之前定義的常數成員:可以參考其他列舉成員的值,如果它們已經是常數。
    • 帶括號的常數列舉表示式 : 比如 (2 + 3) * 5。
    • +,-, ~ 一元運算子應用於常數列舉表示式。
    • +, -, *, /, %, <<, >>, >>>, &, |, ^ 二元運算子,常數列舉表示式做為其一個操作物件。

      若常數列舉表示式求值後為 NaNInfinity,則會在編譯階段報錯

類別

傳統方法中,JavaScript 透過建構函式實現類別的概念,透過原型鏈實現繼承。而在 ES6 中,我們終於迎來了 class。( MDN - 繼承與原型鏈 )

TypeScript 除了實現了所有 ES6 中的類別的功能以外,還添加了一些新的用法。

類別的概念

  • 類別 ( Class ):類別是一種藍圖,定義了某個事物的屬性和行為(方法)。 比如,如果我們有一個「車子」的類別,那麼這個類別會包含「車子」的一些特徵(顏色、型號)以及它能做的事情(開動、停下)。

  • 物件(Object):物件是類別的具體實例。 你可以把類別想像成一個模型,而物件就是根據這個模型創建出來的具體事物。比如,你可以用 new 關鍵字來根據「車子」的類別創建出一個具體的「我的車子」物件。

  • 面向物件(OOP)的三大特性:封裝繼承多型

    • 封裝(Encapsulation):將內部的資料和行為隱藏起來,只暴露給外部使用的方法。這樣可以保護資料不被隨意修改。比如車子的引擎工作細節是封裝起來的,我們只需要按下「啟動」按鈕就能開車,無需知道內部運作。

    • 繼承(Inheritance):子類別可以繼承父類別的特性和行為,並且還可以擴展修改這些行為。比如,「電動車」類別可以繼承「車子」類別的所有功能,並增加「充電」的方法。

    • 多型(Polymorphism):子類別可以根據自己的特性,實現不同版本的相同行為。比如「貓」和「狗」都繼承了「動物」類別,但它們各自有不同的 eat() 方法,適合自己吃東西的方式。

  • 存取器(getter & setter):存取器用來控制屬性值的讀取和設定行為。你可以定義一個 getter 來控制物件屬性如何被讀取,或用 setter 來控制如何更改屬性值。

  • 修飾符(Modifiers):修飾符用來限制類別中屬性或方法的存取範圍。

    常見的修飾符有:

    • public:屬性或方法可以被任何人存取
    • private:屬性或方法只能在類別內部使用,外部無法存取
    • protected:屬性或方法可以在子類別中使用,但外部無法直接存取
  • 抽象類別(Abstract Class):抽象類別是一個不能直接創建實例的類別,必須由其他類別繼承。它通常用來定義一些基本的行為,但具體的實作必須由子類別來完成。像是「動物」類別可以是抽象的,然後「貓」和「狗」類別來實現具體的 eat() 行為。

  • 介面(Interfaces):不同類別之間公有的屬性或方法,可以抽象成一個介面。介面可以被類別實現(implements)。

一個類別只能繼承自另一個類別,但是可以實現多個介面

總結

這些概念能幫助我們將程式分成小的、易於理解的部分。

  • 類別提供了結構,封裝保護了資料。
  • 繼承與多型提供了彈性。
  • 存取器與修飾符則進一步控制程式行為。
  • 抽象類別與介面讓我們能夠建立清晰的設計規範。

TypeScript 中類別的用法

public private 和 protected

TypeScript 可以使用三種訪問修飾符(Access Modifiers),分別是 public、private 和 protected。

  • public 修飾的屬性或方法是公有的,可以在任何地方被訪問到,預設所有的屬性和方法都是 public 的

  • private 修飾的屬性或方法是私有的,不能在宣告它的類別的外部訪問

  • protected 修飾的屬性或方法是受保護的,它和 private 類似,區別是它在子類別中也是允許被訪問的

當建構函式修飾為 private 時,該類別不允許被繼承或者實例化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Animal {
public name;
private constructor (name) {
this.name = name;
}
}
class Cat extends Animal {
constructor (name) {
super(name);
}
}

let a = new Animal('Jack');

// index.ts(7,19): TS2675: Cannot extend a class 'Animal'. Class constructor is marked as private.
// index.ts(13,9): TS2673: Constructor of class 'Animal' is private and only accessible within the class declaration.

當建構函式修飾為 protected 時,該類別只允許被繼承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Animal {
public name;
protected constructor (name) {
this.name = name;
}
}
class Cat extends Animal {
constructor (name) {
super(name);
}
}

let a = new Animal('Jack');

// index.ts(13,9): TS2674: Constructor of class 'Animal' is protected and only accessible within the class declaration.

修飾符還可以使用在建構函式引數中,等同於類別中定義該屬性,使程式碼更簡潔 :

1
2
3
4
5
6
class Animal {
// public name: string;
public constructor (public name) {
this.name = name;
}
}

readonly

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Animal {
readonly name;
public constructor(name) {
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.

--------------------

class Animal {
// public readonly name;
public constructor(public readonly name) {
this.name = name;
}
}

注意如果 readonly 和其他訪問修飾符同時存在的話,需要寫在其後面。

抽象類別

abstract 用於定義抽象類別和其中的抽象方法。

抽象類別是不允許被實例化的:

1
2
3
4
5
6
7
8
9
10
11
abstract class Animal {
public name;
public constructor(name) {
this.name = name;
}
public abstract sayHi();
}

let a = new Animal('Jack');

// index.ts(9,11): error TS2511: Cannot create an instance of the abstract class 'Animal'.

抽象類別中的抽象方法必須被子類別實現:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 錯誤
abstract class Animal {
public name;
public constructor(name) {
this.name = name;
}
public abstract sayHi();
}

// 沒有實現抽象方法 sayHi
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'.

---------------------

// 正確
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');

類別與介面

TypeScript 新手指南 - 類別與介面

泛型

泛型(Generics)是指在定義函式、介面或類別的時候,不預先指定具體的型別,而在使用的時候再指定型別的一種特性。

Array<any> 允許陣列的每一項都為任意型別。但是我們預期的是,陣列中每一項都應該是輸入的 value 的型別。

這時候,泛型就派上用場了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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']

----------------------

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']

多個型別引數

定義泛型的時候,可以一次定義多個型別引數:

1
2
3
4
5
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]];
}

swap([7, 'seven']); // ['seven', 7]

泛型約束

在函式內部使用泛型變數的時候,由於事先不知道它是哪種型別,所以不能隨意的操作它的屬性或方法:

1
2
3
4
5
6
7
// 泛型 T 不一定包含屬性 length,所以編譯的時候報錯了
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'.

這時,我們可以對泛型進行約束,只允許這個函式傳入那些包含 length 屬性的變數。這就是泛型約束:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface Lengthwise {
length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}

-------------------

// 使用了兩個型別引數,其中要求 T 繼承 U,這樣就保證了 U 上不會出現 T 中不存在的欄位。
function copyFields<T extends U, U>(target: T, source: U): T {
for (let id in source) {
target[id] = (<T>source)[id];
}
return target;
}

let x = { a: 1, b: 2, c: 3, d: 4 };

copyFields(x, { b: 10, d: 20 });

泛型介面

TypeScript 新手指南 - 泛型介面

泛型類別

TypeScript 新手指南 - 泛型類別

宣告合併

TypeScript 新手指南 - 宣告合併