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