Appearance
类型兼容
类型兼容性是 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
上面代码中,x
和 y
类型虽然有区别,但是它们都是空的,什么都没有。所以它们是兼容的。
把这个例子改变一下,增加一个成员,就能看出是如何工作的了:
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