TS兼容性
兼容性的原则是Duck-Check,
首先要理解什么是TS的兼容性?
被赋值的变量的类型中的属性 在赋值源的类型 中都存在 类型检查就会通过
接口的兼容性
函数传入的 变量类型 与 声明类型 不匹配TS会进行兼容性检查
interface Animal {
name: string;
age: number;
}
interface Person {
name: string;
age: number;
gender: number
}
// 要判断目标类型`Person`是否能够兼容输入的源类型`Animal`
function getName(animal: Animal): string {
return animal.name;
}
let p = {
name: 'lzy',
age: 25,
gender: 0
}
getName(p);
// 只有在传参的时候两个变量之间才会进行兼容性的比较,
// 赋值的时候并不会比较,会直接报错
let a: Animal = {
name: 'lzy',
age: 25,
gender: 0 // 报错
}
在第三篇文章 TS接口 中有写道,
对象字面量 直接作为参数时 会经历 额外属性检查
而改写成 对象 再作为参数 传入函数调用,则不会 检查对象上的额外属性,
很显然,调用函数直接传入 对象字面量,相当于是对形参(对变量)的直接赋值,而
对 变量赋值 不会进行兼容性检查,不容许额外属性,直接报错,
函数调用 传参,对于 传入一个 值为对象的变量 这种情况,却是宽容的,允许兼容
猜测:
具有额外属性字面量作为对象 赋值给参数,再作为参数传入函数调用,TS不报错,
是因为TS认为在对象赋值的时候,已经做过变量检查了,是安全的,所以允许兼容.
基本类型的兼容性
//基本数据类型也有兼容性判断
let num : string|number;
let str:string='zhufeng';
num = str;
//只要有toString()方法就可以赋给字符串变量
let num2 : {
toString():string
}
let str2:string='jiagou';
num2 = str2;
类的兼容性
类有 静态部分 和 实例部分 的类型
在比较两个变量的 类类型时,只有实例的成员会被比较
类的私有成员和受保护成员会影响兼容性。
当检查类实例的兼容时,如果目标类型包含一个私有成员,
那么源类型必须包含来自同一个类的这个私有成员。
同样地,这条规则也适用于包含受保护成员实例的类型检查。
这允许子类赋值给父类,但是不能赋值给其它有同样类型的类。
class Animal {
feet: number;
// 如果下面的name前面加了public等,则 a = s会报错,因为加了修饰符会被加入实例的一部分
constructor(name: string,numFeet: number) { }
}
class Size {
feet: number;
// 如果下面的 numFeet 和 上面的 numFeet 都加了public,两个等式依旧 不报错
// 如果下面的 numFeet 和 上面的 numFeet 都加了private,两个等式 都报错
constructor(numFeet: number) { }
}
let a: Animal;
let s: Size;
a = s; // OK
s = a; // OK
在类的实例部分比较时,只对比实例部分的结构而不在意类型名
class Animal{ name:string }
class Bird extends Animal{ swing:number }
let a:Animal = new Bird();
//并不是java的那套 '父类兼容子类,但子类不兼容父类'
//仅仅是因为bird 需要的属性 swing:number, new Animal()没有
let b:Bird = new Animal(); //报错
下面举例说明 与 类型是不是子类父类 无关,只要结构符合就行
class Animal{ name:string }
//如果父类和子类结构一样,也可以的
class Bird extends Animal{}
let a:Animal = new Bird();
let b:Bird = new Animal();
// 两个完全无关的类也一样,可以相互赋值,只要结构符合
函数的兼容性
函数赋值给变量时 先比较函数的参数 再比较函数的返回值
type sumFunc = (a:number,b:number)=>number;
let sum:sumFunc;
function f1(a:number,b:number):number{
return a+b;
}
//可以省略一个参数
function f2(a:number):number{
return a;
}
sum = f2;
//可以省略二个参数
function f3():number{
return 0;
}
//多一个参数可不行
function f4(a:number,b:number,c:number){
return a+b+c;
}
sum = f1;
sum = f2;
sum = f3;
sum = f4; // 报错
// 返回值的对象 可以多属性,不能比变量要求的返回值少属性
给变量赋值时 赋值的函数 的参数要求 只可比变量要求函数参数 少
给变量赋值时 赋值的函数 的返回值要求 可比变量要求函数参数 多
函数加上 参数类型的父子类的兼容
注意:逆变与协变是 函数实体 赋值给 具有约束的变量时产生的检查
在给限定了 函数类型的变量 赋值函数时
赋值给 变量的 函数
其参数 可以是 比变量类型定义里 要求的属性 更少(逆变)
返回值 可以是 比变量类型定义里 要求的属性 更多(协变)
简化版:
__给变量赋值的函数 参数可以属性更少,返回值可以属性更多__。
class Parent {
house() { }
}
class Child extends Parent {
car() { }
}
class Grandson extends Child {
sleep() { }
}
type Arg<T> = (arg: T) => void;
type Return<T> = (arg: any) => T;
type isArg = Arg<Parent> extends Arg<Child> ? true : false; // 逆变
type isReturn = Return<Grandson> extends Return<Child> ? true : false; // 协变
理解:
用于赋值的函数的 参数 的属性 必须 比 被赋值的变量的要求 更少
用于赋值的函数的 返回值 的属性 必须 比 被赋值的变量的要求 更多
为什么这么要求?
因为在实际应用中,
在类型检查时,只检查 变量的类型,而不是 检查 变量被赋的值的类型(因为值是可变的,关键是值在变量赋值时已被检查过)
如果不要求 用于赋值的函数的 返回值 的属性 只能等于或更多
变量的 后续使用时 会出现,
类型检查时,允许 使用返回值的某属性(因为,变量类型约束里写了有啊),
编译执行时,发现返回值的这个属性undefined(因为,变量的真实函数值返回值比变量类约束的值少啊!!)
而执行报错。
如果不要求 用于赋值的函数的 参数 的属性 只能等于或更少
变量的 后续使用时 则会出现
类型检查时,允许 使用 变量A 进行变量调用(因为,变量类型约束里 变量对参数的属性要求 A这个参数都有啊)
编译执行时,发现 变量A 少了某个属性(因为,变量的真实函数值 需要的参数属性 确实 比变量类型约束里多啊!!)
而执行报错
class Animal{}
class Dog extends Animal{
public name:string = 'Dog'
}
class BlackDog extends Dog {
public age: number = 10
}
class WhiteDog extends Dog {
public home: string = '北京'
}
let animal: Animal;
let blackDog: BlackDog;
let whiteDog: WhiteDog;
// 报错,:参数dog 与 参数blackDog不兼容,
// blackDog参数限定的函数需要age属性,dog参数限定的变量 不强制要求,
// 假设此处不报错
// 到时候通过 childToChild变量,调用函数传参可以是dog类型
// 但其函数值却需要blackDog类型的age属性,运行时可能报错undefined
const childToChild: (dog: Dog)=>Dog = (blackDog: BlackDog): BlackDog => {
// 同时此处 返回值 换成了具有更多属性的 blackDog,此处返回值没有问题。
return new BlackDog()
}
// 报错:参数dog 与 参数blackDog不兼容,blackDog需要age,dog不需要。
const childToParent: (dog: Dog)=>Dog = (BlackDog: BlackDog): Animal => {
// 报错:同时此处 返回值 换成了具有更少属性的 blackDog,此处返回值也错了。
return new Animal()
}
// 参数是 需要属性更少的aniamal,此处参数没有问题
const parentToParent: (dog: Dog)=>Dog = (animal: Animal): Animal => {
// 报错:返回值不能是属性更少的animal类型,只能是属性更多的类
return new Animal()
}
// 完全OK,用于赋值的函数 参数的类型属性要求更少 返回值参数的类型属性更多
const parentToChild: (dog: Dog)=>Dog = (animal: Animal): BlackDog => {
return new BlackDog()
}
//(Animal → Greyhound) ≼ (Dog → Dog)
泛型的兼容性
首先明确,接口就是一种类型 一种type,泛型得先转换成具体type再进行判断
原则:先根据 传入的泛型T确定其具体的类型,再进行兼容性判断
//1.接口内容为空 或者 没用到泛型 或者 传入的T一样
// 或者 y的T是x的T的子类时,是相等的
interface Empty<T>{
name:string
}
let x!:Empty<string>;
let y!:Empty<number>;
x = y;
//2.接口内容不为空的时候不相等的
interface NotEmpty<T>{
data:T
}
let x1!:NotEmpty<string>; // !非空断言
let y1!:NotEmpty<number>;
x1 = y1; // 报错
//demo2中就相当于是下面这样
interface NotEmptyString{
data:string
}
interface NotEmptyNumber{
data:number
}
let xx2!:NotEmptyString;
let yy2!:NotEmptyNumber;
xx2 = yy2; // 报错
如果是没有指定类型的泛型,则视为T为any
let identity = function<T>(x: T): T { }
let reverse = function<U>(y: U): U { }
identity = reverse; // OK, because (x: any) => any matches (y: any) => any
枚举的兼容性
枚举类型 与 数字类型 相互兼容
不同枚举类型 不兼容
//数字可以赋给枚举
enum Colors {Red=1,Yellow=2}
enum Colors2 {Red=1,Yellow=2}
let c:Colors;
let c2:Colors2;
c = Colors.Red;
c = 1;
c = '1'; // 报错
c = Colors2.Red // 报错
//枚举值可以赋给数字
let n:number;
n = 1;
n = Colors.Red;