首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入类型系统_TypeScript笔记8

深入类型系统_TypeScript笔记8

作者头像
ayqy贾杰
发布2019-06-12 15:10:12
9330
发布2019-06-12 15:10:12
举报
文章被收录于专栏:黯羽轻扬黯羽轻扬

一.类型推断

赋值推断

类型推断机制减轻了强类型带来的语法负担,例如:

let x = 3;
// 等价于
let x: number = 3;

编译器能够根据变量初始值3推断出变量类型是number,因此多数场景下不必显式声明类型,它猜得到

P.S.即使在一切都要提前确定类型的Haskell中,也并非处处充斥着类型声明,而是相当简洁,正是因为编译器提供了强大的类型推断支持

在类似赋值的场景能够根据目标值来确定类型,具体如下:

  • 变量或(类)成员初始值
  • 参数默认值
  • 函数返回值

这3类值都能提供直接的类型信息,进而确定目标类型。除此之外,还有一些不那么直接的场景,比如数组类型

数组类型
let x = [0, 1, null];

数组中的元素除了number就是null,而number“兼容”null,因此推断x的类型是number[]

Null、Undefined和Never是其它类型的子类型,因此可以赋值给任何其它类型变量

(摘自基本类型_TypeScript笔记2)

也就是说,要确定数组类型的话,先要确定每个元素的类型,再考虑其兼容关系,最终确定一个最“宽”的类型(包容数组中所有其它类型,称为best common type)作为数组类型

如果数组元素中没有一个能够兼容其它所有类型的类型(即找不出best common type),就用联合类型,例如:

// 推断 mixin: (string | number | boolean)[]
let mixin = [1, '2', true];class Animal {}
class Elephant extends Animal {}
class Snake extends Animal {}
// 推断 zoo: (Elephant | Snake)[]
let zoo: Animal[] = [new Elephant(), new Snake()];

上下文推断

与赋值推断相比,上下文推断是另一种不同的思路:

    推断
值 ------> 变量类型
      查找             匹配(推断)
上下文 -----> 上下文类型 -----------> 变量类型

前者从值到类型,后者从类型到类型。根据上下文得出类型信息,再按位置映射到变量上,例如:

// 推断 mouseEvent: MouseEvent
window.onmousedown = function(mouseEvent) {
 // ...
};

右侧匿名函数作为mousedown事件处理器,遵从DOM API的类型约束,从而得出参数类型:

interface MouseEvent extends UIEvent {
 readonly clientX: number;
 readonly clientY: number;
 //...等等很多属性
}
interface GlobalEventHandlers {
 onmousedown: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
}
interface Window extends GlobalEventHandlers {/*...*/}declare var window: Window;

(摘自TypeScript/lib/lib.dom.d.ts)

如果脱离了mousedown事件处理器这个上下文,就推断不出参数类型了:

// 推断 mouseEvent: any
function handler(mouseEvent) {
 console.log(mouseEvent.clickTime);
}

很多场景都会根据上下文推断类型,例如:

  • 函数调用中的参数
  • 赋值语句的右侧
  • 类型断言
  • 对象成员和数组字面量
  • return语句

二.子类型兼容性

TypeScript的13种基本类型中,类型层级关系如下:

简单总结:

  • Any最“宽”。兼容其它所有类型(换言之,其它类型都是Any的子类型)
  • Never最“窄”。不兼容任何其它类型
  • Void兼容Undefined和Null
  • 其它类型都兼容Never和Void

P.S.兼容可以简单理解可否赋值(文末有严谨描述),例如:

let x: any;
let y: number;
let z: null;// Any兼容Number
x = y;
// Number兼容Null
y = z;
// Null不兼容Number
// 错误 Type 'number' is not assignable to type 'null'.
z = y;

不只基本类型有层级,函数、类、泛型等复杂类型间也有这样的兼容关系

三.函数

兼容性判定

对类型系统而言,需要准确判断一个函数类型是否兼容另一个函数类型,例如在赋值的场景:

let log: (msg: string) => void
let writeToFile: (msg: any, encode: string) => void// 类型兼容吗?该不该报错
log = writeToFile;
writeToFile = log;

从类型安全角度来看,把log换成writeToFile不安全(缺encode参数,writeToFile不一定能正常工作),反过来的话是安全的,因为返回值类型相同,参数绰绰有余,msg的类型也兼容(stringany的子类型)

具体的,TypeScript类型系统对函数类型的兼容性判定规则如下:

  • 参数:要求对应参数的类型兼容,数量允许多余 let x = (a: number) => 0; let y = (b: number, s: string) => 0;y = x; // OK x = y; // Error
  • 返回值:要求返回值类型兼容 let x = () => ({name: "Alice"}); let y = () => ({name: "Alice", location: "Seattle"});x = y; // OK y = x; // Error, because x() lacks a location property
  • 函数类型:要求满足双变性约束

函数类型的双变性(bivariance)

双变是指同时满足协变和逆变,简单地讲:

  • 协变(covariant):允许出现父类型的地方,也允许出现子类型,即里氏替换原则
  • 逆变(contravariant):协变反过来,即允许出现子类型的地方,也允许出现父类型
  • 双变(bivariant):同时满足协变和逆变
  • 不变(invariant或nonvariant):既不满足协变也不满足逆变

协变很容易理解,子类型兼容父类型,此外还具有一些(父类型不具备的)扩展属性或方法,因此用子类型换掉父类型后,仍能正常工作(类型安全)

而逆变并不很直观,什么场景下,用父类型换掉子类型后,仍能保证类型安全呢?

继承关系中的成员函数重写,算是逆变的典型例子:

class Example {
 foo(maybe: number | undefined) { }
 str(str: string) { }
 compare(ex: Example) { }
}class Override extends Example {
 foo(maybe: number) { } // Bad: should have error.
 str(str: 'override') { } // Bad: should have error.
 compare(ex: Override) { } // Bad: should have error.
}

(摘自Overridden method parameters are not checked for parameter contravariance)

对比重写前后的函数类型:

// foo
(maybe: number | undefined) => any
(maybe: number) => any
// str
(str: string) => any
(str: 'override') => any
// compare
(ex: Example) => any
(ex: Override) => any

P.S.str(str: 'override')str(str: string)“窄”个undefined,默认值使得参数值集少了undefined

参数都从“宽”的类型变成了更“窄”的类型,即从父类型变为子类型,显然,这样做是不安全的,例如:

function callFoo(example: Example) {
 return example.foo(undefined);
}callFoo(new Example());   // 没问题
callFoo(new Override());  // 可能会出错,因为子类的foo不接受undefined

相反地,如果子类重写后的参数类型更“宽”,那么就是安全的,例如:

class Example {
 foo(maybe: number | undefined) { }
}class Override extends Example {
 foo(maybe: number) { }  // Sound
}

这就是所谓的逆变,对成员函数的参数而言,用父类型换掉子类型是安全的,即:

允许出现子类型的地方,也允许出现父类型

从类型角度来看,子类型允许类型之间有层级(继承)关系,从宽泛类型到特殊类型,而协变、逆变等关系就建立在这种类型层级之上:

  • 协变:简单类型的层级关系保留到了复杂类型,这个复杂类型就是协变的,例如AnimalCat的父类型,而数组Animal[]也是Cat[]的父类型,所以数组类型是协变的
  • 逆变:简单类型的层级关系到复杂类型中反过来了,这个复杂类型就是逆变的。例如函数类型Animal => stringCat => string的子类型(因为后者接受的参数更“窄”),而简单类型AnimalCat的父类型,那么函数类型就是逆变的

P.S.如我们所见,逆变并不直观,因此为了保持类型系统简单,有的语言也认为类型构造器不变,虽然这样可能会违反类型安全

特殊地,TypeScript里的函数类型是双变的,例如:

interface Comparer<T> {
 compare(a: T, b: T): number;
}declare let animalComparer: Comparer<Animal>;
declare let dogComparer: Comparer<Dog>;animalComparer = dogComparer;  // Ok because of bivariance
dogComparer = animalComparer;  // Ok

(摘自Strict function types)

理论上应该要求函数参数逆变,以确保类型安全,因此:

// 把父类型赋值给子类型,在逆变的场景中是安全的
dogComparer = animalComparer;  // Ok
// 把子类型赋值给父类型,在逆变的场景(函数类型)中是不安全的
animalComparer = dogComparer;  // Ok because of bivariance

后者不安全,但在JavaScript世界里很常见:

This is unsound because a caller might end up being given a function that takes a more specialized type, but invokes the function with a less specialized type. In practice, this sort of error is rare, and allowing this enables many common JavaScript patterns.

所以TypeScript并没有强制约束函数类型逆变,而是允许双变。更进一步地,在比较两个函数类型时,只要一方参数兼容另一方的参数即可,如上例中dogCompareranimalComparer能够相互赋值

可选参数和剩余参数

比较参数兼容性时,不要求匹配可选参数,比如原类型具有额外的可选参数是合法的,目标类型缺少相应的可选参数也是合法的

对于剩余参数,就当成是无限多个可选参数,也不要求严格匹配。虽然从类型系统的角度来看不安全,但在实际应用中是一种相当常见的“模式”,例如用不确定的参数调用回调函数:

function invokeLater(args: any[], callback: (...args: any[]) => void) {
 /* ... Invoke callback with 'args' ... */
}// Unsound - invokeLater "might" provide any number of arguments
invokeLater([1, 2], (x, y) => console.log(x + ", " + y));

函数重载

对于存在多个重载的函数,要求源函数的每个重载版本在目标函数上都有对应的版本,以保证目标函数可以在所有源函数可调用的地方调用,例如:

interface sum {
 (a: number, b: number): number;
 (a: number[]): number;
}// Sum要求的两个版本
function add(a: any, b: any);
function add(a: any[], b?: any): any;
// 额外的版本
function add(a: any[], b: any, c: number): any;
function add(a, b) { return a; }
let sum: sum = add;

sum函数有两个重载版本,所以目标函数至少要兼容这两个版本

四.枚举

首先,来自不同枚举类型的枚举值不兼容,例如:

enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };let s = Status.Ready;
// Type 'Color.Green' is not assignable to type 'Status'.
s = Color.Green;  // Error

特殊地,数值枚举与数值相互类型兼容,例如:

enum Status { Ready, Waiting };
// 数值兼容枚举值
let ready: number = Status.Ready;
// 枚举值兼容数值
let waiting: Status = 1;

但字符串枚举并不与字符串类型相互兼容

enum Status { Ready = '1', Waiting = '0' };
let ready: string = Status.Ready;
// 报错 Type '"0"' is not assignable to type 'Status'.
let waiting: Status = '0';

P.S.虽然从实际类型上看,上例赋值是合法的,但在类型系统中认为二者不兼容,因此报错

五.类

类与对象字面量类型和接口类似,区别在于,类同时具有实例类型和静态类型,而比较两个类实例时,仅比较实例成员

因此,静态成员和构造函数并不影响兼容性:

class Animal {
 static id: string = 'Kitty';
 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

私有成员和受保护成员

成员修饰符privateprotected也会影响类型兼容性,具体地,要求这些成员源自同一个类,以此保证父类兼容子类:

class Animal {
 private feet: number;
 constructor() { }
}class Cat extends Animal { }// 和Animal长得完全一样的Tree
class Tree {
 private feet: number;
 constructor() { }
}// 正确 父类兼容子类
let animal: Animal = new Cat();
// 错误 Type 'Tree' is not assignable to type 'Animal'.
animal = new Tree();
// 正确 二者“形状”完全一样
let kitty: Cat = new Animal();

Tree实例赋值给Animal类型会报错,虽然二者看起来完全一样,但私有属性feet源自不同的类:

Types have separate declarations of a private property ‘feet’.

同样的,上例中把Animal实例赋值给Cat类型之所以不报错,是因为二者成员列表相同,并且私有属性feet也源自同一个Animal

六.泛型

interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string>;x = y;  // OK, because y matches structure of x
y = x;  // OK, because x matches structure of y

尽管Empty<number>Empty<string>差别很大,但泛型定义中并没有用到类型参数T(类似于unused variable,没什么意义),因此互相兼容

interface NotEmpty<T> {
   data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;// 错误 Type 'Empty<string>' is not assignable to type 'Empty<number>'.
x = y;

此时,指定了类型参数的泛型与一般具体类型一样严格比较,对于未指定类型参数的泛型,就当类型参数是any,再进行比较,例如:

let identity = function<T>(x: T): T {
 //...
 return x;
}
let reverse = function<U>(y: U): U {
 //...
 return y;
}// 正确 等价于把(y: any) => any赋值给(x: any) => any
identity = reverse;

七.类型兼容性

实际上,TypeScript规范中只定义了2种兼容性,子类型兼容性与赋值兼容性,二者存在细微的区别:

Assignment extends subtype compatibility with rules to allow assignment to and from any, and to and from enum with corresponding numeric values.

赋值兼容性扩展了子类型兼容性,允许any相互赋值,以及enum和对应数值相互赋值

至于类型兼容性,规范中并未定义这个概念,在多数语境下,所谓的类型兼容性遵从赋值兼容性,implementsextends子句也不例外

参考资料

  • Type Inference
  • Type Compatibility
  • Covariance and contravariance (computer science)
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-02-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端向后 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一.类型推断
    • 赋值推断
      • 数组类型
    • 上下文推断
    • 二.子类型兼容性
    • 三.函数
      • 兼容性判定
        • 函数类型的双变性(bivariance)
          • 可选参数和剩余参数
            • 函数重载
            • 四.枚举
            • 五.类
              • 私有成员和受保护成员
              • 六.泛型
              • 七.类型兼容性
                • 参考资料
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档