TS類型兼容性

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(); 
  • 名義類型中TeacherPerson并不兼容, 因為沒有聲明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 場景

  • 這個特點在我們處理已有類型時是很有用的,如這樣的場景: 我們要處理視頻和圖片兩類資源,對如下已有的類型PicVideo各自組成的兩個列表picListvideoList進行排序,排序依據是他們的寬和高:
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中,我們實際上并不需要顯式的讓PicVideo繼承canBeSorted接口。

因為從結構上來看,我們的PicVideo都是擁有widthheight屬性的,因此canBeSorted一定是兼容我們的PicVideo屬性的。也就是說:在TS中,我們這樣寫出的排序函數是支持直接傳入與之兼容的Pic[]Video[]的。不需要我們對類型進行顯式的繼承聲明。

因此,這樣的修改是可以正常工作的,而且既不會降低函數的可復用性,也不需要我們修改已有類型的定義。

看到這里,你應該能理解TS的結構類型兼容性的概念和怎么應用了。
注意: 下一小節是在理解了上述概念之后的思考,建議先搞懂上述概念。

1.5 個人一點小思考(手動劃重點)

在與朋友討論了一下是否應該顯式繼承之后,有了一些'這樣寫能否正常工作'之外的思考:

既然Pic繼承或者不繼承canBeSorted都可以,那這兩種寫法有什么區別呢?什么時候應該繼承,什么時候不應該繼承呢?

我們來轉化一下這兩種寫法下,上面的排序函數的語意:

  • Pic繼承canBeSorted時,排序函數的語意: 一個對列表進行排序的工具函數,該列表中的成員必須繼承canBeSorted類型
  • 不繼承時,排序函數的語意: 一個對列表進行排序的工具函數,該列表中的成員必須包含widthheight屬性

這兩者雖然都解決了當前的問題,但實際上對于各個階段的代碼修改的影響是不同的:

  1. 就解決目前的場景來說:
  • 不使用繼承所需要的修改量非常小,很簡單就能滿足需求。
  • 而使用繼承時,我們需要對已有的類型進行統一的修改,使之顯式的繼承
  1. 當我們需要再為這個函數擴充一個場景,新增一個類型Card,并為同樣存在寬高的Card列表進行排序時:
  • 不使用繼承時,我們需要為Card定義它所有的屬性(包括寬高),即canBeSorted中寫過的東西再寫一遍
  • 而使用繼承時,我們只需要定義除了寬高之外的額外屬性即可
// Do not use inheritance
interface Card {
  width: number;
  height: number;
  // other props ...
}

// Use inheritance
interface Card extends canBeSorted {
  // other props ...
}
  1. 當我們需要修改這個排序函數,除了寬高,新的排序函數還需要基于這些類型共有的另一個新增字段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 

函數兼容性

  • 未完待續
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容