前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入 TypeScript 中的子类型,进阶 Vue3 源码前必须搞懂的。

深入 TypeScript 中的子类型,进阶 Vue3 源码前必须搞懂的。

作者头像
ssh_晨曦时梦见兮
发布2020-10-26 15:54:31
9951
发布2020-10-26 15:54:31
举报
文章被收录于专栏:前端从进阶到入院

前言

TypeScript 中有很多地方涉及到子类型 subtype、父类型 supertype 的概念,如果搞不清这些概念,那么很可能被报错搞的无从下手,或者在写一些复杂类型的时候看到别人可以这么写,但是不知道为什么他可以生效。(就是我自己没错了)

子类型

比如考虑如下接口:

代码语言:javascript
复制
interface Animal {
  age: number;
}

interface Dog extends Animal {
  bark(): void;
}

在这个例子中,AnimalDog 的父类,DogAnimal的子类型,子类型的属性比父类型更多,更具体。

  • 在类型系统中,属性更多的类型是子类型。
  • 在集合论中,属性更少的集合是子集。

也就是说,子类型是父类型的超集,而父类型是子类型的子集,这是直觉上容易搞混的一点。

记住一个特征,子类型比父类型更加具体,这点很关键。

可赋值性 assignable

assignable 是类型系统中很重要的一个概念,当你把一个变量赋值给另一个变量时,就要检查这两个变量的类型之间是否可以相互赋值。

代码语言:javascript
复制
let animal: Animal;
let dog: Dog;

animal = dog; // ✅ok
dog = animal; // ❌error! animal 实例上缺少属性 'bark'

从这个例子里可以看出,animal 是一个「更宽泛」的类型,它的属性比较少,所以更「具体」的子类型是可以赋值给它的,因为你是知道 animal 上只有 age 这个属性的,你只会去使用这个属性,dog 上拥有 animal 所拥有的一切类型,赋值给 animal 是不会出现类型安全问题的。

反之,如果 dog = animal,那么后续使用者会期望 dog 上拥有 bark 属性,当他调用了 dog.bark() 就会引发运行时的崩溃。

从可赋值性角度来说,子类型是可以赋值给父类型的,也就是 父类型变量 = 子类型变量 是安全的,因为子类型上涵盖了父类型所拥有的的一切属性。

当我初学的时候,我会觉得 T extends {} 这样的语句很奇怪,为什么可以 extends 一个空类型并且在传递任意类型时都成立呢?当搞明白上面的知识点,这个问题也自然迎刃而解了。

在函数中的运用

假设我们有这样的一个函数:

代码语言:javascript
复制
function f(val: { a: number; b: number });
复制代码

有这样两个变量:

代码语言:javascript
复制
let val1 = { a: 1 };
let val2 = { a: 1, b: 2, c: 3 };

调用 f(val1) 是会报错的,比较显而易见的来看是因为缺少属性 b,而函数 f 中很可能去访问 b 属性并且做一些操作,比如 b.substr(),这就会导致崩溃。

换成上面的知识点来看,val1 对应的类型是{ a: number },它是 { a: number, b: number } 的父类型,调用 f(val1) 其实就相当于把函数定义中的形参 val 赋值成了 val1, 把父类型的变量赋值给子类型的变量,这是危险的。

反之,调用 f(val2) 没有任何问题,因为 val2 的类型是 val类型的子类型,它拥有更多的属性,函数有可能使用的一切属性它都有。

假设我现在要开发一个 redux,在声明 dispatch 类型的时候,我就可以这样去做:

代码语言:javascript
复制
interface Action {
  type: string;
}

declare function dispatch<T extends Action>(action: T);
复制代码

这样,就约束了传入的参数一定是 Action 的子类型。也就是说,必须有 type,其他的属性有没有,您随意。

在联合类型中的运用

学习了以上知识点,再看联合类型的可赋值性,乍一看会比较反直觉, 'a' | 'b' | 'c''a' | 'b' 的子类型吗?它看起来属性更多诶?其实正相反,'a' | 'b' | 'c''a' | 'b' 的父类型。因为前者比后者更「宽泛」,后者比前者更「具体」。

代码语言:javascript
复制
type Parent = "a" | "b" | "c";
type Son = "a" | "b";

let parent: Parent;
let son: Son;

parent = son; // ✅ok
son = parent; // ❌error! parent 有可能是 'c'

这里 son 是可以安全的赋值给 parent 的,因为 son 的所有可能性都被 parent 涵盖了。

而反之则不行,parent 太宽泛了,它有可能是 'c',这是 Son 类型 hold 不住的。

这个例子看完以后,你应该可以理解为什么 'a' | 'b' extends 'a' | 'b' | 'c' 为 true 了,在书写 conditional types的时候更加灵活的运用吧。

结语

这篇文章结合我自己最近学习类型相关知识的一些心得整理而成,如果有错误或者疏漏欢迎大家指出。在有了这些作为前置知识,后续我们就可以进一步讨论 协变逆变双向协变 等类型系统中的深入知识点,才能理解以下的这段代码是如何把 union type 转为 intersection type 的。

代码语言:javascript
复制
type UnionToIntersection = (U extends any
? (k: U) => void
: never) extends (k: infer I) => void
  ? I
  : never;
复制代码

如果这些知识点有人感兴趣的话,后续我应该会进一步的深入整理文章,TypeScript 的类型世界真的是其乐无穷的。

参考资料

Subsets & Subtypes

TypeScript 官方文档

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 子类型
  • 可赋值性 assignable
  • 在函数中的运用
  • 在联合类型中的运用
  • 结语
  • 参考资料
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档