组合类型与类型保护_TypeScript笔记9

一.组合类型

交叉类型(intersection types)

组合多个类型产生新类型,源类型间存在“与”关系,例如:

interface ObjectConstructor {
  assign<T, U>(target: T, source: U): T & U;
}

(摘自TypeScript/lib/lib.es2015.core.d.ts

Object.assign能把source: U身上的可枚举属性浅拷贝到target: T上,因此返回值类型为T & U

交叉类型A & B既是A也是B,因此具有各个源类型的所有成员:

interface A {
  a: string;
}
interface B {
  b: number
}

let x: A & B;
// 都是合法的
x.a;
x.b;

P.S.虽然名字叫intersection(交集),实际上是“求并集”

联合类型(union types)

类似于交叉类型,联合类型由具有“或”关系的多个类型组合而成,例如:

interface DateConstructor {
  new (value: number | string | Date): Date;
}

(摘自TypeScript/lib/lib.es2015.core.d.ts

Date构造函数接受一个numberstringDate类型的参数,对应类型为number | string | Date

联合类型A | B要么是A要么是B,因此只有所有源类型的公共成员(“交集”)才能访问:

interface A {
  id: 'a';
  a: string;
}
interface B {
  id: 'b';
  b: number
}

let x: A | B;
x.id;
// 错误 Property 'a' does not exist on type 'A | B'.
x.a;
// 错误 Property 'b' does not exist on type 'A | B'.
x.b;

二.类型保护

联合类型相当于由类型构成的枚举类型,因而无法确定其具体类型:

联合类型A | B要么是A要么是B

这在函数签名上没什么问题,但在函数实现中,通常需要区分出具体类型,例如:

let createDate: (value: number | string | Date) => Date;
createDate = function(value) {
  let date: Date;
  if (typeof value === 'string') {
    value = value.replace(/-/g, '/');
    // ...
  }
  else if (typeof value === 'number') {/*...*/}
  else if (value instanceof Date) {/*...*/}

  return date;
};

因此,在此类场景下,需要把“宽”的联合类型,“缩窄”到一个具体类型。从类型角度来看,上面代码在理想情况下应该是这样的:

function(value) {
  // 此处,value是联合类型,要么number要么string要么Date

  if (typeof value === 'string') {
    // 此分支下,value是string
  }
  else if (typeof value === 'number') {
    // 此分支下,value是number
  }
  else if (value instanceof Date) {
    // 此分支下,value是Date
  }

  // 此处,value是联合类型,要么number要么string要么Date
}

也就是说,需要有一种机制能让我们告诉类型系统,“听着,现在我知道这个东西的具体类型了,请把它圈小一些”

而这种机制,就是类型保护(type guard)

A type guard is some expression that performs a runtime check that guarantees the type in some scope.

typeof类型保护

typeof variable === 'type'是用来确定基本类型的惯用手法,因此TypeScript能够识别typeof,并自动缩窄对应分支下的联合类型:

let x: number | string;
if (typeof x === 'string') {
  // 正确 typeof类型保护,自动缩窄到string
  x.toUpperCase();
}

switch语句,&&等其它分支结构中也同样适用:

switch (typeof x) {
  case 'number':
    // 正确 typeof类型保护
    x.toFixed();
    break;
}
// 正确 typeof类型保护
typeof x !== 'number' && x.startsWith('xxx');

注意,最后一例很有意思,x要么是number要么是string,从typeof判断得知不是number,因此缩窄到string

具体的,typeof类型保护能够识别两种形式的typeof

  • typeof v === "typename"

  • typeof v !== "typename"

并且typename只能是numberstringbooleansymbol,因为其余的typeof检测结果不那么可靠(具体见typeof),所以不作为类型保护,例如:

let x: any;
if (typeof x === 'function') {
  // any类型,typeof类型保护不适用
  x;
}
if (typeof x === 'object') {
  // any类型,typeof类型保护不适用
  x;
}

P.S.相关讨论,见typeof a === “object” does not type the object as Object

instanceof类型保护

类似于typeof检测基本类型,instanceof用来检测实例与“类”的所属关系,也是一种类型保护,例如:

let x: Date | RegExp;
if (x instanceof RegExp) {
  // 正确 instanceof类型保护,自动缩窄到RegExp实例类型
  x.test('');
}
else {
  // 正确 自动缩窄到Date实例类型
  x.getTime();
}

具体的,要求instanceof右侧是个构造函数,此时左侧类型会被缩窄到:

  • 该类实例的类型(构造函数prototype属性的类型)

  • (构造函数存在重载版本时)由构造函数返回类型构成的联合类型

例如:

// Case1 该类实例的类型
let x;
if (x instanceof Date) {
  // x从any缩窄到Date
  x.getTime();
}

// Case2 由构造函数返回类型构成的联合类型
interface DateOrRegExp { 
  new(): Date;
  new(value?: string): RegExp;
}

let A: DateOrRegExp;
let y;
if (y instanceof A) {
  // y从any缩窄到RegExp | Date
  y;
}

P.S.关于instanceof类型保护的更多信息,见4.24 Type Guards

P.S.另外,class具有双重类型含义,在TypeScript代码里的体现形式如下:

  • 类的类型:typeof className

  • 类实例的类型:typeof className.prototype或者className

例如:

class A {
  static prop = 'prop';
  id: 'b'
}

// 类的类型
let x: typeof A;
x.prop;
// 错误 id是实例属性,类上不存在
x.id;

// 类实例的类型
let y: typeof A.prototype;
let z: A;
// 二者类型等价
z = y;
// 错误 prop是静态属性,实例上不存在
z.prop;
z.id;

也就是说,类实例的类型等价于构造函数prototype属性的类型。但这仅在TypeScript的编译时成立,与JavaScript运行时概念有冲突:

class A {}
class B extends A {}
// 构造函数prototype属性是父类实例,其类型是父类实例的类型
B.prototype instanceof A === true

自定义类型保护

typeofinstanceof类型保护能够满足一般场景,对于一些更加特殊的,可以通过自定义类型保护来缩窄类型:

interface RequestOptions {
  url: string;
  onSuccess?: () => void;
  onFailure?: () => void;
}

// 自定义类型保护,将参数类型any缩窄到RequestOptions
function isValidRequestOptions(opts: any): opts is RequestOptions {
  return opts && opts.url;
}

let opts;
if (isValidRequestOptions(opts)) {
  // opts从any缩窄到RequestOptions
  opts.url;
}

自定类型保护与普通函数声明类似,只是返回类型部分是个类型谓词(type predicate)

parameterName is Type

其中parameterName必须是当前函数签名中的参数名,例如上面的opts is RequestOptions

调用带类型谓词的函数后,传入参数的类型会被缩窄到指定类型,与前两种类型保护行为一致:

let isNumber: (value: any) => value is number;

let x: string | number;
if (isNumber(x)) {
  // 缩窄到number
  x.toFixed(2);
}
else {
  // 不是number就是string
  x.toUpperCase();
}

三.Nullable与联合类型

TypeScript里空类型(Void)有两种:Undefined与Null,是(除Never外)其它所有类型的子类型。因此nullundefined可以赋值给其它任何类型:

let x: string;
x = null;
x = undefined;
// 运行时错误,编译时不报错
x.toUpperCase();

从类型上看,Nullable类型相当于原类型与null | undefined组成的联合类型(上例中,相当于let x: string | null | undefined;

这意味着类型检查不那么十分可靠,因为仍无法避免undefined/null.xxx之类的错误

strictNullChecks

针对空类型的潜在问题,TypeScript提供了--strictNullChecks选项,开启之后会严格检查空类型:

let x: string;
// 错误 Type 'null' is not assignable to type 'string'.
x = null;
// 错误 Type 'undefined' is not assignable to type 'string'.
x = undefined;

对于可以为空的类型,需要显式声明:

let y: string | undefined;
y = undefined;
// Type 'null' is not assignable to type 'string | undefined'.
y = null;

同时,可选参数和可选属性会自动带上| undefined,例如:

function createDate(value?: string) {
  // 错误 Object is possibly 'undefined'.
  value.toUpperCase();
}

interface Animal {
  color: string;
  name?: string;
}
let x: Animal;
// 错误 Type 'undefined' is not assignable to type 'string'.
x.color = undefined;
// 错误 Object is possibly 'undefined'.
x.name.toUpperCase();

类似的空值相关问题都能够暴露出来,由此看来,空类型严格检查相当于一种编译时检查追溯空值的能力

!后缀类型断言

既然Nullable类型实质上是联合类型,那么同样面临类型缩窄的问题。对此,TypeScript也提供了符合直觉的类型保护:

function createDate(value: string | undefined) {
  // 缩窄到string
  value = value || 'today';
  value.toUpperCase();
}

对于自动类型保护无法处理的场景,可以简单地通过!后缀去掉| undefined | null成分:

function fixed(name: string | null): string {
  function postfix(epithet: string) {
    // 通过!去掉类型中的null成分,使之缩窄到string
    return name!.charAt(0) + '.  the ' + epithet; // ok
  }
  name = name || "Bob";
  return postfix("great");
}

identifier!相当于类型断言(不同于类型保护):

let x: string | undefined | null;
x!.toUpperCase();
// 相当于
(<string>x).toUpperCase();
// 或者
(x as string).toUpperCase();
// Object is possibly 'null' or 'undefined'.
x.toUpperCase();

P.S.类型断言与类型保护的区别在于,断言是一次性的(或者说是临时的),而类型保护在一定作用域下都有效

参考资料

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code