5.TS兼容性


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;

文章作者: 罗紫宇
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 罗紫宇 !
  目录