TS类型兼容性

2021-01-25  本文已影响0人  Gaarahan

1. 基于结构类型的兼容 -- 排序函数类型的优化

引用自TS中文文档-类型兼容性 :

TypeScript里的类型兼容性是基于结构子类型的。结构类型是一种只使用其成员来描述类型的方式。 它正好与名义(nominal)类型形成对比。(译者注:在基于名义类型的类型系统中,数据类型的兼容性或等价性是通过明确的声明和/或类型的名称来决定的。这与结构性类型系统不同,它是基于类型的组成结构,且不要求明确地声明。)

1.1 背景

// 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(); 
class Teacher {
    String name;
    int age;
    String studentID;
}
// Error :  incompatible types: Teacher cannot be converted to Person
Person person = new Teacher(); 
class Teacher {
    name: string;
    age: number;
    studentID: string;
}
const person: Person = new Teacher();

1.2 场景

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该如何定义呢?

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都可以,那这两种写法有什么区别呢?什么时候应该继承,什么时候不应该继承呢?

我们来转化一下这两种写法下,上面的排序函数的语意:

这两者虽然都解决了当前的问题,但实际上对于各个阶段的代码修改的影响是不同的:

  1. 就解决目前的场景来说:
  1. 当我们需要再为这个函数扩充一个场景,新增一个类型Card,并为同样存在宽高的Card列表进行排序时:
// Do not use inheritance
interface Card {
  width: number;
  height: number;
  // other props ...
}

// Use inheritance
interface Card extends canBeSorted {
  // other props ...
}
  1. 当我们需要修改这个排序函数,除了宽高,新的排序函数还需要基于这些类型共有的另一个新增字段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 

函数兼容性

上一篇 下一篇

猜你喜欢

热点阅读