Skip to content

类型兼容

类型兼容性是 Typescript 中的核心原则之一。 它用于确定一个类型可以赋值给另一个类型。 在 TypeScript 中,如果目标类型包含一些类型,则源类型也必须包含相同的类型。

ts
interface Named {
  name: string;
}

class Person {
  name: string;
}

let p: Named;
// OK, because of structural typing
p = new Person();

上面代码中,Person 类包含一个 name 属性,Named 接口类型中也有一个 name 属性,它们是兼容的。

函数兼容

在 TypeScript 中,如果一个函数的参数是兼容的,那么就可以把参数更改为更具体的类型。

ts
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;

y = x; // OK
x = y; // Error

上面代码中,y 函数的参数比 x 函数的参数更具体。

参数个数

ts
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;

y = x; // OK
x = y; // Error

上面代码中,y 函数的参数比 x 函数的参数更具体。

参数类型

函数的参数类型兼容性是基于结构子类型的。这意味着,如果两个函数的参数类型兼容,那么这两个函数就是兼容的。以下是关于参数类型兼容性的一些规则:

  • 如果源函数的参数类型是目标函数参数类型的子集,那么源函数和目标函数就是兼容的。
  • 如果两个函数的参数类型是子类型关系,那么它们也是兼容的。例如,如果一个函数接受 Animal 类型的参数,另一个接受 Dog 类型的参数(Dog 是 Animal 的子类型),那么它们是兼容的。
ts
  let x = (a: number) => 0;
  let y = (b: number, s: string) => 0;

  y = x; // OK
  x = y; // Error

上面代码中,y 函数的参数比 x 函数的参数更具体。

返回值类型

函数的返回值类型兼容性是基于结构子类型的。这意味着,如果两个函数的参数类型兼容,并且它们的返回值类型也兼容,那么这两个函数就是兼容的。

ts
let x = () => ({name: 'Alice'});
let y = () => ({name: 'Alice', location: 'Seattle'});

x = y; // OK
y = x; // Error

上面代码中,y 函数的返回值比 x 函数的返回值更具体。

函数参数双向协变

当比较函数参数类型时,只有当源函数参数能够赋值给目标函数或者反过来时才能赋值成功。 这是不稳定的,因为调用者可能传入了一个具有更精确类型信息的函数,但是调用这个传入的函数的时候却使用了不是那么精确的类型信息。 实际上,这极少会发生错误,并且能够实现很多JavaScript里的常见模式。

ts
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
let z = (...args: any[]) => 0;

x = y; // OK
y = x; // Error

z = x; // OK
x = z; // OK

上面代码中,z 函数的参数比 x 函数的参数更具体。

枚举类型

枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的。比如,

ts
enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };

let status = Status.Ready;
status = Color.Green;  // Error

上面代码中,Status 枚举类型与 Color 枚举类型不兼容。

TypeScript 中的接口(Interfaces)是一种用于定义对象形状的类型。在 TypeScript 中,接口的兼容性是基于结构子类型的,这意味着如果两个接口的结构相似,那么它们就是兼容的。 这里有几种接口兼容的情况:
相同属性:如果两个接口有相同的属性,那么它们是兼容的。
额外属性:目标接口可以拥有源接口中没有的额外属性。
可选属性:如果目标接口有一个可选属性,那么源接口可以不提供这个属性。
只读属性:源接口的只读属性可以赋值给目标接口的普通属性,但反过来则不行。
让我们来看一些具体的例子:

ts
class Animal {
  feet: number;
  constructor(name: string, numFeet: number) { }
}

class Size {
  feet: number;
  constructor(numFeet: number) { }
}

let a: Animal;
let s: Size;

a = s;  // OK
s = a;  // Error

上面代码中,Animal 类的实例可以赋值给 Size 类的实例。但是 Size 类的实例就不可以赋值给 Animal 类的实例。

泛型

在 TypeScript 中,泛型(Generics)提供了一种方式来创建可重用的组件,这些组件可以与多种类型一起工作。泛型类型的兼容性遵循 TypeScript 的结构类型系统,但是有一些特殊情况需要考虑。
泛型类型的兼容性通常取决于泛型类型参数的具体化(具体化是指在实例化泛型类型时提供的具体类型)。在泛型类型参数没有被具体化时,TypeScript 会使用一种叫做“类型参数多态”的规则,这种规则允许不同但结构兼容的泛型类型之间进行赋值。

ts
interface Empty<T> {
}

let x: Empty<number>;
let y: Empty<string>;

x = y;  // OK
y = x;  // OK

上面代码中,xy 类型虽然有区别,但是它们都是空的,什么都没有。所以它们是兼容的。
把这个例子改变一下,增加一个成员,就能看出是如何工作的了:

ts
interface NotEmpty<T> {
    data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;

x = y;  // error, x and y are not compatible

在这里,泛型类型在使用时就好比不是一个泛型类型。

对于没指定泛型类型的泛型参数时,会把所有泛型参数当成any比较。 然后用结果类型进行比较,就像上面第一个例子。比如,

ts
let identity = function<T>(x: T): T {
    // ...
}

let reverse = function<U>(y: U): U {
    // ...
}

identity = reverse;  // Okay because (x: any)=>any matches (y: any)=>any

仅用于培训和测试,通过使用本站代码内容随之而来的风险与本站无关。版权所有,未经授权请勿转载,保留一切权利。
ICP备案号:滇ICP备15009214号-13   公安网备:滇公网安备 53312302000061号