1. 基于結構類型的兼容 -- 排序函數類型的優化
引用自TS中文文檔-類型兼容性 :
TypeScript里的類型兼容性是基于結構子類型的。結構類型是一種只使用其成員來描述類型的方式。 它正好與名義(nominal)類型形成對比。(譯者注:在基于名義類型的類型系統中,數據類型的兼容性或等價性是通過明確的聲明和/或類型的名稱來決定的。這與結構性類型系統不同,它是基于類型的組成結構,且不要求明確地聲明。)
- 文檔中的這段話描述了一個特點: TS中,兩個類型兼容,并不需要進行顯式的繼承或實現。只要其結構上是兼容的即可。
1.1 背景
- 名義類型(Java, C#), 結構性類型中,
Person
都是兼容Student
的:
// java
class Person {
String name;
int age;
}
class Student extends Person {
String studentID;
}
Person person = new Student();
// typescript
class Person {
name: string;
age: number;
}
class Student extends Person {
studentID: string;
}
const person: Person = new Student();
- 名義類型中
Teacher
與Person
并不兼容, 因為沒有聲明Teacher extends Person
:
class Teacher {
String name;
int age;
String studentID;
}
// Error : incompatible types: Teacher cannot be converted to Person
Person person = new Teacher();
- 但在TS中,
Person
也是兼容Teacher
的,因為結構上,Teacher
擁有Person
的所有字段,且類型相同:
class Teacher {
name: string;
age: number;
studentID: string;
}
const person: Person = new Teacher();
1.2 場景
- 這個特點在我們處理已有類型時是很有用的,如這樣的場景: 我們要處理視頻和圖片兩類資源,對如下已有的類型
Pic
和Video
各自組成的兩個列表picList
與videoList
進行排序,排序依據是他們的寬和高:
interface Pic {
name: string;
width: number;
height: number;
mainColor: string;
// .... other props belong picture
}
interface Video {
name: string;
width: number;
height: number;
during: string;
// .... other props belong video
}
因為他們的排序方式是相同的,所以我們實現了一個排序函數,來對一個有寬高屬性的列表進行排序,并返回對應類型的排序后的列表,以此避免重復的排序代碼:
const sortByWidthAndHeight = function (list: canBeSorted): canBeSorted {
let sortedList = [];
// sort by height and width
return sortedList;
}
1.3 問題
但此時,類型canBeSorted
該如何定義呢?
- 你可能會覺得,使用聯合類型
PicList | VideoList
是個不錯的選擇:
type canBeSorted = Pic[] | Video[];
的確,聯合類型可以解決當前的問題, 但實際上這樣的做法在一定程度上降低了該函數的可復用性。注意,我們設計這個函數的目的是對一個有寬高屬性的列表進行排序,而不是只對Pic[] 或 Video[]進行排序。
- 相比之下繼承是一個不錯的選擇, 不會帶來上述的問題:
interface canBeSorted {
width: number;
height: number;
}
interface Pic extends canBeSorted {
// ....
}
interface Video extends canBeSorted {
// ....
}
const sortByWidthAndHeight = function (list: canBeSorted[]): canBeSorted[] {
let sortedList = [];
// sort by height and width
return sortedList;
}
這樣的修改方式在java
這樣的基于名義類型兼容的語言中是常見的,我們需要修改之前定義好的類型,顯式的繼承我們定義的基礎可排序接口。
1.4 類型兼容性的應用
- 結合我們這部分提到的結構性兼容,我們可以對上面的例子進行合適的改進:
interface canBeSorted {
width: number;
height: number;
}
interface Pic {
// ....
}
interface Video {
// ....
}
const sortByWidthAndHeight = function (list: canBeSorted[]): canBeSorted[] {
let sortedList = [];
// sort by height and width
return sortedList;
}
還記得開頭我們提到的TS類型兼容的規則嗎?在TS中,我們實際上并不需要顯式的讓Pic
和Video
繼承canBeSorted
接口。
因為從結構上來看,我們的Pic
和Video
都是擁有width
和height
屬性的,因此canBeSorted
一定是兼容我們的Pic
和Video
屬性的。也就是說:在TS中,我們這樣寫出的排序函數是支持直接傳入與之兼容的Pic[]
或Video[]
的。不需要我們對類型進行顯式的繼承聲明。
因此,這樣的修改是可以正常工作的,而且既不會降低函數的可復用性,也不需要我們修改已有類型的定義。
看到這里,你應該能理解TS的結構類型兼容性的概念和怎么應用了。
注意
: 下一小節是在理解了上述概念之后的思考,建議先搞懂上述概念。
1.5 個人一點小思考(手動劃重點)
在與朋友討論了一下是否應該顯式繼承之后,有了一些'這樣寫能否正常工作'之外的思考:
既然Pic
繼承或者不繼承canBeSorted
都可以,那這兩種寫法有什么區別呢?什么時候應該繼承,什么時候不應該繼承呢?
我們來轉化一下這兩種寫法下,上面的排序函數的語意:
-
Pic
繼承canBeSorted
時,排序函數的語意: 一個對列表進行排序的工具函數,該列表中的成員必須繼承canBeSorted
類型。 - 不繼承時,排序函數的語意: 一個對列表進行排序的工具函數,該列表中的成員必須包含
width
和height
屬性。
這兩者雖然都解決了當前的問題,但實際上對于各個階段的代碼修改的影響是不同的:
- 就解決目前的場景來說:
- 不使用繼承所需要的修改量非常小,很簡單就能滿足需求。
- 而使用繼承時,我們需要對已有的類型進行統一的修改,使之顯式的繼承。
- 當我們需要再為這個函數擴充一個場景,新增一個類型
Card
,并為同樣存在寬高的Card
列表進行排序時:
- 不使用繼承時,我們需要為
Card
定義它所有的屬性(包括寬高),即將canBeSorted
中寫過的東西再寫一遍 - 而使用繼承時,我們只需要定義除了寬高之外的額外屬性即可。
// Do not use inheritance
interface Card {
width: number;
height: number;
// other props ...
}
// Use inheritance
interface Card extends canBeSorted {
// other props ...
}
- 當我們需要修改這個排序函數,除了寬高,新的排序函數還需要基于這些類型共有的另一個新增字段
size
進行排序時:
- 不使用繼承時,我們需要在每個類型中都添加一次
size
屬性 - 而使用繼承時,我們只需要在
canBeSorted
中定義一次size
屬性。
觀察了這些場景,我們可以發現: 一開始使用繼承時,我們雖然寫了更多的代碼。但隨著場景不斷擴充,繼承所帶來的好處會逐漸體現出來。
因此,這里得出結論: 在該工具函數不需要進行額外的場景擴充時,可以直接依靠結構類型兼容來進行快速且有效的定義。但當需要考慮可擴展性時,我們應該優先使用繼承。
1.6 一點不屬于兼容性討論的小修改,可忽略:
- 到這里,我們關于類型定義的討論就結束了。然后再為排序函數加上泛型,以添加傳入列表與傳出列表類型相同的約束,一個優雅的排序函數的類型定義就誕生了:
const sortByWidthAndHeight = function<T extends canBeSorted>(list: T[]): T[] {
let sortedList = [];
// sort by height and width
return sortedList;
}
const sortedList = sortByWidthAndHeight<Pic>(picList); // good
const sortedList = sortByWidthAndHeight<Video>(videoList); // good
const sortedList = sortByWidthAndHeight<Video>(picList); // bad
函數兼容性
- 未完待續