一.组合类型
交叉类型(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
构造函数接受一个number
或string
或Date
类型的参数,对应类型为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
只能是number
、string
、boolean
或symbol
,因为其余的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
自定义类型保护
typeof
与instanceof
类型保护能够满足一般场景,对于一些更加特殊的,可以通过自定义类型保护来缩窄类型:
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外)其它所有类型的子类型。因此null
和undefined
可以赋值给其它任何类型:
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.类型断言与类型保护的区别在于,断言是一次性的(或者说是临时的),而类型保护在一定作用域下都有效