当前位置: 首页 > 科技观察

深入TypeScript中的子类型、逆变和协变,进阶Vue3源码前必须了解的内容

时间:2023-03-18 18:26:51 科技观察

转载本文请联系前端从进阶到入场公众号。前言TypeScript中有很多地方涉及子类型subtype和父类型supertype的概念。如果不理解这些概念,可能会被错误搞糊涂,或者看到别人在写一些复杂类型的时候也可以这样写。但是不知道为什么会这样。(我说的对)子类型例如,考虑以下接口:子类型比父类型具有更多属性,并且更具体。在类型系统中,具有更多属性的类型是子类型。在集合论中,属性较少的集合是子集。换句话说,子类型是超类型的超集,超类型是子类型的子集,直观上容易混淆。重要的是要记住,子类型比超类型更具体。Assignable可赋值是类型系统中的一个重要概念。当你将一个变量赋值给另一个变量时,你需要检查两个变量的类型是否可以相互赋值。letanimal:Animalletdog:Doganimal=狗//?okdog=动物//?错误!动物实例上缺少属性“吠声”从这个例子中可以看出,动物是一种“更广泛”的类型,具有更少的属性。所以可以给它分配更多“特定”的子类型,因为你知道animal上只有age这个属性,你只会用到这个属性。Dog具有animal的所有类型,因此分配给animal是不正确的。会有类型安全问题。相反,如果dog=animal,那么后续用户会期望狗有吠叫属性,当他调用dog.bark()时,会导致运行时崩溃。从可赋值的角度来看,子类型可以赋值给父类型,即父类型变量=子类型变量是安全的,因为子类型覆盖了父类型的所有属性。初学的时候,会觉得Textends{}这个语句很奇怪。为什么它可以扩展一个空类型,并且在传递任何类型时都是如此?明白了以上知识点,这个问题自然就迎刃而解了。在函数中的应用假设我们有这样一个函数:functionf(val:{a:number;b:number})有两个变量:letval1={a:1}letval2={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。在声明dispatchtype的时候,我可以这样:interfaceAction{type:string}declarefunctiondispatch(action:T)这样传入的参数必须是ActionSubtype。也就是说必须要有类型,有没有其他的属性就看你的了。在关节类型中的应用学习完以上知识点,再来看关节类型的可赋值,乍一看似乎有悖常理,'a'|'b'|'c'是'a'的孩子|'b'类型?好像属性多了点?它实际上是相反的,'a'|'b'|'c'是'a'的父类型|'b'。因为前者比后者更“广泛”,后者比前者更“具体”。typeParent='a'|'b'|'c'typeSon='a'|'b'letparent:Parentletson:Sonparent=son//?okson=parent//?报错!parent可能是'c',其中son可以安全地分配给parent,因为son的所有可能性都被parent覆盖。但反之亦然,parent太宽泛,可能是'c',Son类型无法容纳。看完这个例子,你应该能明白为什么要用'a'|'b'扩展'a'|'b'|'c'为true,所以在写条件类型的时候可以更灵活的使用它。逆变和协变的定义首先来自维基百科[1]:协变和逆变(Covarianceandcontravariance)是计算机科学中通过类型构造函数描述具有父/子类型关系的多个类型,它们之间是否存在父/子类型关系构建了多个复杂类型。描述的比较晦涩,但是用我们上面动物类型的例子来解释一波。现在我们还有两个父子类型,Animal和Dog。Covariance那么想象一下,现在我们分别有这两个子类型的数组,它们之间的父子关系应该是怎样的呢?是的,Animal[]仍然是Dog[]的父类型,对于这段代码,将子类型赋给父类型还是安全的:letanimals:Animal[]letdogs:Dog[]animals=dogsanimals[0].age//?ok转成数组后,对于变量的父类型,我们还是只去那些必须是Dog类型的属性。那么,对于类型构造函数typeMakeArray=T[],就是Covariance。逆变有两个函数:letvisitAnimal=(animal:Animal)=>void;letvisitDog=(dog:Dog)=>void;animal=dog是类型安全的,所以visitAnimal=visitDog似乎可行?事实上,想象一下这两个函数的实现:letvisitAnimal=(animal:Animal)=>{animal.age}letvisitDog=(dog:Dog)=>{dog.agedog.bark()}由于参数visitDog是具有吠叫属性的更具体的子类型,因此如果visitAnimal=visitDog,我们可以将没有吠叫属性的普通动物类型传递给visitDog。visitAnimal=visitDogleanimal={age:5}visitAnimal(animal)//?这会导致运行时错误,animal.bark根本不存在,调用这个方法会导致崩溃。但反过来,visitDog=visitAnimal完全没问题。因为后续调用者传入了比动物属性更具体的狗,所以函数体内的所有访问都是安全的。分别为Animal和Dog类型调用如下类型构造函数后:typeMakeFunction=(arg:T)=>void父子类型关系反转,即Contravariance。在TS中,当然在TypeScript中,由于灵活性等权衡,函数参数的默认处理是双向协变。也就是说,visitAnimal=visitDog和visitDog=visitAnimal都可以使用。只有开启tsconfig中的strictFunctionType后,才会严格按照逆变来限制赋值关系。