11.TypeScript中的类型兼容性

类型兼容性

类型兼容性用于确定一个类型能否赋值给其他类型。TypeScript里的类型兼容性是基于结构子类型的。 结构类型是一种只使用其成员来描述类型的方式。 它正好与名义(nominal)类型形成对比(在基于名义类型的类型系统中,数据类型的兼容性或等价性是通过明确的声明和/或类型的名称来决定的。这与结构性类型系统不同,它是基于类型的组成结构,且不要求明确地声明。)。通常来说结构相同,类型也就兼容。

如下面的string类型和number类型是不兼容的,因此它们之前不能相互赋值。

let str: string = '123';
let num: number = 123;

num = str; // error 不能将类型“string”分配给类型“number”。
str = num; // error 不能将类型“number”分配给类型“string”

但是下面的Named接口和Person类,它们之间的类型是兼容的。

interface Named {
    
    
    name: string;
}

class Person {
    
    
    name: string;
}

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

在这里Named和Person都有成员name,且都是number类型,因为它们是兼容的,可以赋值。

稳定性

TypeScript类型系统的设计运行你有一些不正确的行为。例如,任何类型都能被赋值给any,意味着编译器允许你做任何想做的事情。

let foo: any = 'foo';
foo = 123; // ok

结构化

TypeScript对象是一种结构化的类型,这就意味着只要结构匹配,名称也就无所谓了,编译器就认为它们类型是兼容的。比如上面例子中的Named和Person。下面这个例子也说明了这一点:

interface Point {
    
    
    x: number;
    y: number
}

class Point2D {
    
    
    constructor(public x: number, public y: number) {
    
     }
}

let p: Point = new Point2D(1, 2); // ok 结构化的类型

这种结构化类型需要满足最小结构原则,也即是赋值目标结构至少要满足源目标的所有结构(可以有额外的成员):

interface Point2D {
    
    
    x: number;
    y: number
}

interface Point3D {
    
    
    x: number;
    y: number;
    z: number
}


const point2D = {
    
     x: 0, y: 0 };
const point3D = {
    
     x: 0, y: 0, z: 0 };

function iTakePoint2D(point: Point2D) {
    
    
    // do something
}

iTakePoint2D(point2D) // ok 完全匹配
iTakePoint2D(point3D) // ok 有额外属性z
iTakePoint2D({
    
     x: 0 }) // error 缺少属性y

变体

对类型兼容分析来说,变体是一个很容易理解,而且很重要的概念。

对简单的类型Base和Child来说,如果Child是Base的子类,则Child的实例能被赋值给Base类型的变量:

class Base {
    
    
    name: string
}

class Child extends Base{
    
    
    name: string
}

let base:Base;
let child = new Child();

base = child; // ok 多态性

在由Base和Child组合的复杂类型的兼容性中,它取决于相似场景下的Base与Child的变体。

  • 协变(Covariant):只在同一个方向兼容。
  • 逆变(Contravariant):只在相反的方向上兼容
  • 双向协变(Bivariant):包括同一个方向和不同方向的兼容
  • 不变(Invariant):如果类型不完全相同,则它们是不兼容的。

函数

比较两个函数的时候,下面是一些需要考虑的事情。

返回类型

协变(Covariant):返回类型必须包含足够的数据。

interface Point2D {
    
    
    x: number;
    y: number
}

interface Point3D {
    
    
    x: number;
    y: number;
    z: number
}

let iMakePoint2D = (): Point2D => ({
    
     x: 0, y: 0 });
let iMakePoint3D = (): Point3D => ({
    
     x: 0, y: 0, z: 0 });

iMakePoint2D = iMakePoint3D; // ok
iMakePoint3D = iMakePoint2D // error 不能将类型“() => Point2D”分配给类型“() => Point3D”。类型 "Point2D" 中缺少属性 "z",但类型 "Point3D" 中需要该属性。

参数数量

相对来讲,在比较原始类型和对象类型的时候是比较容易理解的,问题是如何判断两个函数是兼容的。 下面我们从两个简单的函数入手,它们仅是参数列表略有不同:

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

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

要查看x是否能赋值给y,首先看它们的参数列表。 x的每个参数必须能在y里找到对应类型的参数。 注意的是参数的名字相同与否无所谓,只看它们的类型。 这里,x的每个参数在y中都能找到对应的参数,所以允许赋值。

第二个赋值错误,因为y有个必需的第二个参数,但是x并没有,所以不允许赋值。

你可能会疑惑为什么允许忽略参数,像例子y = x中那样。 原因是忽略额外的参数在JavaScript里是很常见的。 例如,Array#forEach给回调函数传3个参数:数组元素,索引和整个数组。 尽管如此,传入一个只使用第一个参数的回调函数也是很有用的:

let items = [1, 2, 3];

// Don't force these extra arguments
items.forEach((item, index, array) => console.log(item));

// Should be OK!
items.forEach((item) => console.log(item));

可以看出函数类型兼容,从返回类型来看,赋值操作左边需要被满足,从参数数量来看,赋值操作右边需要被满足。

可选参数和剩余参数

为了方便起见,可选参数(预先确定的)和rest参数(任何数量的参数)是兼容的。

let foo = (x: number, y: number) => {
    
     };
let bar = (x?: number, y?: number) => {
    
     };
let bas = (...args: number[]) => {
    
     };

foo = bar = bas;
bas = bar = foo;

函数参数类型

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

双向协变(Bivariant):旨在支持常见的事件处理场景。例如:

enum EventType {
    
     Mouse, Keyboard }

interface Event {
    
     timestamp: number; }
interface MouseE extends Event {
    
     x: number; y: number }
interface KeyE extends Event {
    
     keyCode: number }

function listenEvent(eventType: EventType, handler: (n: Event) => void) {
    
    
    /* ... */
}

// Unsound, but useful and common
listenEvent(EventType.Mouse, (e: MouseE) => console.log(e.x + ',' + e.y));

// Undesirable alternatives in presence of soundness
listenEvent(EventType.Mouse, (e: Event) => console.log((<MouseE>e).x + ',' + (<MouseE>e).y));
listenEvent(EventType.Mouse, <(e: Event) => void>((e: MouseE) => console.log(e.x + ',' + e.y)));

// Still disallowed (clear error). Type safety enforced for wholly incompatible types
listenEvent(EventType.Mouse, (e: number) => console.log(e));

枚举

枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。

enum Status {
    
     Ready, Waiting };
let ready = Status.Ready;
let num = 1;

num = ready; // ok
ready = num;// ok

不同枚举类型之间是不兼容的。比如:

enum Status {
    
     Ready, Waiting };

enum Color {
    
    
    Red,
    Yellow
}

let state = Status.Ready;
let color = Color.Red;

state = color; // error 不能将类型“Color.Red”分配给类型“Status”。
color = state; // error 不能将类型“Status”分配给类型“Color”

类与对象字面量和接口差不多,但有一点不同:类有静态部分和实例部分的类型。 比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不在比较的范围内。

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;  // OK

类的私有成员和受保护成员

类的私有成员和受保护成员会影响兼容性。 当检查类实例的兼容时,如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员。 同样地,这条规则也适用于包含受保护成员实例的类型检查。 这允许子类赋值给父类,但是不能赋值给其它有同样类型的类。

也就是说私有的和受保护的成功的成员必须来自相同的类。

class Animal {
    
    
    protected feet: number;
}

class Cat extends Animal {
    
     }

let animal: Animal;
let cat: Cat;

animal = cat; // ok
cat = animal; // ok

class Size {
    
    
    protected feet: number
}

let size: Size;

animal = size; //error 不能将类型“Size”分配给类型“Animal”。属性“feet”受保护,但类型“Size”并不是从“Animal”派生的类。
size = animal // error 不能将类型“Animal”分配给类型“Size”。属性“feet”受保护,但类型“Animal”并不是从“Size”派生的类。

泛型

因为TypeScript是结构性的类型系统,类型参数只影响使用其做为类型一部分的结果类型。也即是类型参数只有在被成员使用时,才会影响兼容性。

interface Empty<T> {
    
    
}
let x: Empty<number>;
let y: Empty<string>;

x = y;  // OK, because y matches structure of x

上面代码里,x和y是兼容的,因为它们的结构使用类型参数时并没有什么不同。 把这个例子改变一下,增加一个成员,就能看出是如何工作的了:

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

x = y;  // Error, because x and y are not compatible

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

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

let identity = function<T>(x: T): T {
    
    
    return x;
}

let reverse = function<U>(y: U): U {
    
    
    return y;
}

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

高级主题

子类型与赋值

目前为止,我们使用了“兼容性”,它在语言规范里没有定义。 在TypeScript里,有两种兼容性:子类型和赋值。 它们的不同点在于,赋值扩展了子类型兼容性,增加了一些规则,允许和any来回赋值,以及enum和对应数字值之间的来回赋值。

语言里的不同地方分别使用了它们之中的机制。 实际上,类型兼容性是由赋值兼容性来控制的,即使在implements和extends语句也不例外。

おすすめ

転載: blog.csdn.net/sinat_41212418/article/details/120810748