TS类型兼容 - 学习笔记

191 阅读4分钟

当一个类型 Y 可以被赋值给另一个类型 X 时,我们就可以说类型 X 兼容类型 Y

X 兼容 YX (目标类型)= Y (源类型)

口诀:

  • 结构之间兼容:成员少的兼容成员多的
  • 函数之间兼容:参数多的兼容参数少的

型变

为了方便理解下面的示例,这里引入一套句法:

  • A <: B 指 “A 类型是 B 类型的子类型,或者为同种类型”
  • A >: B 指 “A 类型是 B 类型的超类型,或者为同种类型”

型变有四种方式:

  • 不变:只能是 T
  • 协变:可以是 <: T
  • 逆变:可以是 >: T
  • 双变:可以是 <: T 或 >: T

协变

先看段代码:

在线示例

type User1 = {
  id?: number | string;
  name: string;
};

type User2 = {
  id: number;
  name: string;
};

const user1: User1 = {
  id: '1',
  name: '金小钗1',
};

const user2: User2 = {
  id: 2,
  name: '金小钗2',
};

// id 的类型为:number | undefined
function foo(user: { id?: number; name: string }) {}

/*
  这里报错:
  Argument of type 'User1' is not assignable to parameter of type '{ id?: number; name: string; }'.
  Types of property 'id' are incompatible.
    Type 'string | number' is not assignable to type 'number'.
      Type 'string' is not assignable to type 'number'.ts(2345)
*/
foo(user1);

foo(user2);

在上面代码中,foo(user2)中,user2的属性id类型为number,是number | undefined的子类,实参 <: 形参。


foo(user1)报错了,因为user1的属性id类型为number | string | undefined,是number | undefined的超类。

在类型上,我们说 TypeScript 对结构(对象和类)的属性类型进行了协变,所以对于foo()参数类型,传入参数的类型只能是 <: 形参的

在 TypeScript 中,每个复杂类型的成员都会进行协变,包括对象、类、数组和函数的返回值。

函数逆变

如果函数 A 的参数数量小于或等于函数 B 的参数数量,而且满足下述条件,那么函数 A 是 函数 B 的子类型:

  • A 的 this类型未指定或者 >: B 的 this类型
  • A 的 每一个参数类型 >: B 的 参数类型
  • A 的返回值类型 <: B 的返回值类型

除了返回值类型,其他跟上面协变中的实例代码都是反过来的,这就是逆变。


先看段代码,定义了几个类,它们的关系是:Crow<: Bird<:Animal

在线示例

class Animal {}

class Bird extends Animal {
  chirp() {}
}

class Crow extends Bird {
  caw() {}
}

function clone(f: (b: Bird) => Bird) {}

这时定义几个函数,分别来调用下clone()

function birdToBird(b: Bird): Bird {
  return new Bird();
}

function birdToCrow(b: Bird): Crow {
  return new Crow();
}

function birdToAnimal(b: Bird): Animal {
  return new Animal();
}

clone(birdToBird);
clone(birdToCrow);

/*
  Argument of type '(b: Bird) => Animal' is not assignable to parameter of type '(b: Bird) => Bird'.
    Property 'chirp' is missing in type 'Animal' but required in type 'Bird'.ts(2345)
*/
clone(birdToAnimal);

birdToAnimal的返回值类型是 >: clone参数的返回值类型的。


假如clone是这么实现的,那么你就可以理解它为啥报错了:

function clone(f: (b: Bird) => Bird) {
  const bird = new Bird();
  const birdNew = f(bird);
  // Animal 的示例中根本就没有 chirp 方法
  birdNew.chirp();
}

函数返回类型的协变指一个函数是另一个函数的子类型,即一个函数的返回类型 <: 另一个函数的返回类型。


下面来看看函数参数的类型:

function birdToBird(b: Bird): Bird {
  return new Bird();
}

function crowToBird(b: Crow): Bird {
  return new Bird();
}

function animalToBird(b: Animal): Bird {
  return new Bird();
}

clone(birdToBird);
clone(animalToBird);

/*
  Argument of type '(b: Crow) => Bird' is not assignable to parameter of type '(b: Bird) => Bird'.
    Types of parameters 'b' and 'b' are incompatible.
      Property 'caw' is missing in type 'Bird' but required in type 'Crow'.ts(2345)
*/
clone(crowToBird);

crowToBird的参数类型为Crow,<: Bird


结合clone()的实现,假如crowToBird()是这么实现:

function clone(f: (b: Bird) => Bird) {
  const bird = new Bird();
  const birdNew = f(bird);
  birdNew.chirp();
}

function crowToBird(b: Crow): Bird {
  // 从 clone 方法中传入的参数为 Bird 的实例对象,这个对象中根本没有 caw()
  b.caw();
  return new Bird();
}

clone(crowToBird);

多余属性检查

Typescript 检查一个对象是否可赋值给另一个对象类型时,也涉及到类型拓宽。

对象类型的成员会做协变。但是,如果 TypeScript 严守这个规则,而不做额外的检查,将会导致一个问题,例如:

在线示例

type Options = {
  baseURL:string,
  cacheSize?: number,
  tier?: 'prod'|'dev'
}

class API {
  constructor(private options: Options){}
}

new API({
  baseURL: 'https://xxx',
  /*
    这里报错,说
    Argument of type '{ baseURL: string; tierr: string; }' is not assignable to parameter of type 'Options'.
     Object literal may only specify known properties, 
     but 'tierr' does not exist in type 'Options'. 
     Did you mean to write 'tier'?(2345) 
   */
  tierr: 'prod'
})

在上面代码中

  • 预期类型为{ baseURL:string, cacheSize?: number, tier?: 'prod'|'dev' }
  • 传入类型为{ baseURL: string, tierr: string }

传入的类型是预期类型的子类型,但是却报错了。

新鲜对象字面量类型:指的是 TypeScript 从对象字面量中推导出来的类型。
如果对象字面量有类型断言(使用as),或者把对象字面量赋值给变量,那么新鲜对象字面量类型将拓宽为常规的对象类型,也就不能称其为新鲜对象字面量类型。

TypeScript 捕获这个错误的原因是因为它做了多余属性检查,具体过程是:尝试把一个新鲜对象字面量类型 T 赋值给另一个类型 U 时,如果 T 中存在 U 没有的属性,则 TypeScript 报错。

看看下面的例子,就知道什么情况下会做多余属性检查,什么时候不会:

在线示例

type Options = {
  baseURL: string,
  cacheSize?: number,
  tier?: 'prod' | 'dev'
}

class API {
  constructor(private options: Options) { }
}

// 正常行为,没有多余属性
new API({
  baseURL: 'https://xxx',
  tier: 'prod'
})

/*
  因为传入的参数是个字面量对象,并且被 TypeScript 推断为
  新鲜对象字面量类型,所以检查到了多余的属性 badTier
*/
new API({
  baseURL: 'https://xxx',
  // 报错
  badTier: 'prod'
})

// 使用了类型断言,所以正常运行
new API({
  baseURL: 'https://xxx',
  badTier: 'prod'
} as Options)

// 显示注解 Options 类型,在定义的时候,就检查多余类型了
const options: Options = {
  baseURL: 'https://xxx',
  // 报错
  badTier: 'prod'
}
new API(options)