likes
comments
collection
share

TypeScript 子类型

作者站长头像
站长
· 阅读数 49

子类型

如果在期望类型 T 的实例的任何地方,都可以安全地使用类型 S的实例,那么称类型 S 是类型 T 的子类型。

分类

名义子类型

大部分主流编程语言(如 Java 和 C#)采用的方式。在名义子类型中,如果我们使用类似 class S extends T 这样的语法,显式声明一个类型是另一个类型的子类型,这种关系才成立。现在,每当期望有 T 的实例(例如作为函数的实参)时,我们可以使用 S 的一个实例。如果没有将 S 声明extends T,则编译器不会允许我们将其用作 T

结构子类型

TypeScript 使用结构子类型。 在结构子类型中,不需要我们在代码中显式声明子类型关系。只要某个类型 S 包含另外一个类型 T 声明的所有成员,那么前者的实例就可以代替后者的实例使用。换句话说,如果一个类型的结构与另一个类型相似(具有相同的成员,可能还有额外的成员),则它将自动被视为后者的子类型。

顶层和底层类型

顶层类型

如果我们能够把任何值赋给一个类型,就称该类型为顶层类型,因为其他任何类型都是该类型的子类型。换句话说,该类型位于子类型层次结构的顶端。

unknownany 都是顶层类型,unknownany 的区别为:尽管我们可以把任意值赋值给 unknownany,但是在使用这两种类型的变量时,存在一个区别。对于 unknown 的情况,只有当我们确认一个值具有某个类型时,才能把该值用作该类型。对于 any 的情况,我们可以立即把该值用作其他任何类型的值。any 会绕过类型检查。

底层类型

如果一个类型是其他任何类型的子类型,那么我们称之为底层类型,因为它位于子类型层次结构的底端。要成为其他类型的子类型,它必须具有其他类型的成员。因为我们可以有无限个类型和成员,所以底层类型也必须有无限个成员。这是不可能发生的,所以底层类型始终是一个空类型:这是我们不能为其创建实际值的类型。

never 是底层类型,所以我们能把它赋值给其他任何类型。但是我们不能将任何类型赋给 never 类型。

逆变与协变

协变

如果一个类型保留其底层类型的子类型关系,就称该类型具有协变性。数组具有协变性,因为它保留了子类型关系,ST 的子类型,所以 () => S() => T 的子类型

class Animal {}

class Dog extends Animal {
    name = '';
}

type AnimalReturnFunc = () => Animal;
type DogReturnFunc = () => Dog;

const func1: AnimalReturnFunc = () => new Animal();
const func2: DogReturnFunc = () => new Dog();

const func3: AnimalReturnFunc = func2;
// 报错 Type 'AnimalReturnFunc' is not assignable to type 'DogReturnFunc'.
const func4: DogReturnFunc = func1;

因为 DogAnimal 的子类型,所以 DogReturnFuncAnimalReturnFunc 的子类型,所以可以将 func1 赋值给 func4 会提示报错,Type 'AnimalReturnFunc' is not assignable to type 'DogReturnFunc'。我们将 func2 赋值给 func3 就不会有问题。

逆变

如果一个类型颠倒了其底层类型的子类型关系,则称该类型具有逆变性。函数的实参是逆变的,函数之间的关系与其实参类型之间的关系相反。如果ST 的子类型,(argument: S) => void(argument: T) => void 的父类型

class Animal {}

class Dog extends Animal {
    name = '';
}

type AnimalArgumentFunc = (argument: Animal) => void;
type DogArgumentFunc = (argument: Dog) => void;

const func1: AnimalArgumentFunc = (argument: Animal) => {};
const func2: DogArgumentFunc = (argument: Dog) => {};

// 报错 Type 'DogArgumentFunc' is not assignable to type 'AnimalArgumentFunc'.
const func3: AnimalArgumentFunc = func2;
const func4: DogArgumentFunc = func1;

因为 DogAnimal 的子类型,所以 AnimalArgumentFuncDogArgumentFunc 的子类型,所以可以将 func2 赋值给 func3 会提示报错,Type 'DogArgumentFunc' is not assignable to type 'AnimalArgumentFunc'。我们将 func1 赋值给 func4 就不会有问题。

双向协变

如果类型的底层类型的子类型关系决定了它们互为子类型,则称这种类型具有双向协变。

在 TypeScript 中,函数参数也可以是具有双向协变,只需要我们将 strictFunctionTypes 这个选项设置为 false。然后再运行上面逆变的例子,我们既可以将func2 赋值给 func3,也可以将 func1 赋值给 func4。此时 AnimalArgumentFuncDogArgumentFunc 互为子类型。

为什么要支持双向协变

TypeScript 支持双向协变是为了更好地支持类型系统的灵活性和互操作性。有时候我们需要将一个函数类型作为另一个函数类型的参数或返回值类型。在这种情况下,双向协变可以使得 TypeScript 更容易推断出正确的类型,从而避免一些编译时错误。但是需要注意的是,在使用双向协变时,我们需要确保类型安全,避免出现运行时错误。

interface MyEvent {
    type: string;
}

interface MyMouseEvent extends MyEvent {
  x: number;
  y: number;
}

function handleEvent(callback: (event: MyEvent) => void) {
  const event: MyMouseEvent = { type: 'click', x: 10, y: 20 };
  callback(event);
}

// 报错 Argument of type '(event: MyMouseEvent) => void' is not assignable to parameter of type '(event: MyEvent) => void'
handleEvent((event: MyMouseEvent) => {
  console.log(`Mouse clicked at (${event.x}, ${event.y})`);
});

handleEvent((event: MyEvent) => {
  // 报错 Property 'x' does not exist on type 'MyEvent'.
  console.log(`Mouse clicked at (${event.x}, ${event.y})`);
});

例如上面的这里例子,如果我们将 strictFunctionTypes 设置为 true,此时我们 handleEvent 中传入 (event: MyMouseEvent): void 提示类型报错,我们只能将 (event: MyEvent): void 类型的函数传入 handleEvent。但是 MyEvent 又不存在 xy 属性,这是,我们就很难在 TypesScript 不报错的情况下,实现打印鼠标位置的功能。

infer 中的协变与逆变

在条件类型的 extends 中,现在可以用 infer 来推断类型变量。例如,以下提取函数类型的返回类型:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

对于同一类型变量,可以有多个 infer。我们可以将多个协变位置中的同一类型变量推断为联合类型:

type ToUnion<T> = T extends { a: infer U; b: infer U } ? U : never;
// string | number
type Result = ToUnion<{ a: string; b: number }>; 

同样,我们可以将多个逆变位置中的同一类型变量推断为交叉类型:

type ToIntersection<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void }
  ? U
  : never;

// // string & number
type Result = ToIntersection<{ a: (x: string) => void; b: (x: number) => void }>; 

将联合类型转换成交叉类型,就是用了上面将多个逆变位置中的同一类型变量推断为交叉类型的原理。

type UnionToIntersection<T> = 
  (T extends any ? (x: T) => any : never) extends 
  (x: infer R) => any ? R : never
  
  class Animal {}

class Dog extends Animal {
    name = '';
}

// Animal & Dog
type Result = UnionToIntersection<Animal | Dog>
转载自:https://juejin.cn/post/7246581605769904185
评论
请登录