高级类型
交叉类型(Intersection Types)
交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。 例如,Person & Serializable & Loggable同时是Person和Serializable和Loggable。 就是说这个类型的对象同时拥有了这三种类型的成员。
我们大多是在混入(mixins)或其它不适合典型面向对象模型的地方看到交叉类型的使用。 (在JavaScript里发生这种情况的场合很多!) 下面是如何创建混入的一个简单例子("target": "es5"):
function extend<First, Second>(first: First, second: Second): First & Second {
const result: Partial<First & Second> = {};
for (const prop in first) {
if (first.hasOwnProperty(prop)) {
(result as First)[prop] = first[prop];
}
}
for (const prop in second) {
if (second.hasOwnProperty(prop)) {
(result as Second)[prop] = second[prop];
}
}
return result as First & Second;
}
class Person {
constructor(public name: string) { }
}
interface Loggable {
log(name: string): void;
}
class ConsoleLogger implements Loggable {
log(name) {
console.log(`Hello, I'm ${name}.`);
}
}
const jim = extend(new Person('Jim'), ConsoleLogger.prototype);
jim.log(jim.name);联合类型(Union Types)
联合类型与交叉类型很有关联,但是使用上却完全不同。 偶尔你会遇到这种情况,一个代码库希望传入number或string类型的参数。 例如下面的函数:
padLeft存在一个问题,padding参数的类型指定成了any。 这就是说我们可以传入一个既不是number也不是string类型的参数,但是TypeScript却不报错。
在传统的面向对象语言里,我们可能会将这两种类型抽象成有层级的类型。 这么做显然是非常清晰的,但同时也存在了过度设计。 padLeft原始版本的好处之一是允许我们传入原始类型。 这样做的话使用起来既简单又方便。 如果我们就是想使用已经存在的函数的话,这种新的方式就不适用了。
代替any, 我们可以使用联合类型做为padding的参数:
联合类型表示一个值可以是几种类型之一。 我们用竖线(|)分隔每个类型,所以number | string | boolean表示一个值可以是number,string,或boolean。
如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。
这里的联合类型可能有点复杂,但是你很容易就习惯了。 如果一个值的类型是A | B,我们能够确定的是它包含了A和B中共有的成员。 这个例子里,Bird具有一个fly成员。 我们不能确定一个Bird | Fish类型的变量是否有fly方法。 如果变量在运行时是Fish类型,那么调用pet.fly()就出错了。
类型守卫与类型区分(Type Guards and Differentiating Types)
联合类型适合于那些值可以为不同类型的情况。 但当我们想确切地了解是否为Fish时怎么办? JavaScript里常用来区分2个可能值的方法是检查成员是否存在。 如之前提及的,我们只能访问联合类型中共同拥有的成员。
为了让这段代码工作,我们要使用类型断言:
用户自定义的类型守卫
这里可以注意到我们不得不多次使用类型断言。 假若我们一旦检查过类型,就能在之后的每个分支里清楚地知道pet的类型的话就好了。
TypeScript里的类型守卫机制让它成为了现实。 类型守卫就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。
使用类型判定
要定义一个类型守卫,我们只要简单地定义一个函数,它的返回值是一个类型谓词:
在这个例子里,pet is Fish就是类型谓词。 谓词为parameterName is Type这种形式,parameterName必须是来自于当前函数签名里的一个参数名。
每当使用一些变量调用isFish时,TypeScript会将变量缩减为那个具体的类型,只要这个类型与变量的原始类型是兼容的。
注意TypeScript不仅知道在if分支里pet是Fish类型; 它还清楚在else分支里,一定不是Fish类型,一定是Bird类型。
使用in操作符
in操作符in操作符可以作为类型细化表达式来使用。
对于n in x表达式,其中n是字符串字面量或字符串字面量类型且x是个联合类型,那么true分支的类型细化为有一个可选的或必须的属性n,false分支的类型细化为有一个可选的或不存在属性n。
typeof类型守卫
typeof类型守卫现在我们回过头来看看怎么使用联合类型书写padLeft代码。 我们可以像下面这样利用类型断言来写:
然而,必须要定义一个函数来判断类型是否是原始类型,这太痛苦了。 幸运的是,现在我们不必将typeof x === "number"抽象成一个函数,因为TypeScript可以将它识别为一个类型守卫。 也就是说我们可以直接在代码里检查类型了。
这些typeof类型守卫只有两种形式能被识别:typeof v === "typename"和typeof v !== "typename","typename"必须是"number","string","boolean"或"symbol"。 但是TypeScript并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型守卫。
instanceof类型守卫
instanceof类型守卫如果你已经阅读了typeof类型守卫并且对JavaScript里的instanceof操作符熟悉的话,你可能已经猜到了这节要讲的内容。
instanceof类型守卫是通过构造函数来细化类型的一种方式。 比如,我们借鉴一下之前字符串填充的例子:
instanceof的右侧要求是一个构造函数,TypeScript将细化为:
此构造函数的
prototype属性的类型,如果它的类型不为any的话构造签名所返回的类型的联合
以此顺序。
可以为null的类型
null的类型TypeScript具有两种特殊的类型,null和undefined,它们分别具有值null和undefined. 我们在基础类型一节里已经做过简要说明。 默认情况下,类型检查器认为null与undefined可以赋值给任何类型。 null与undefined是所有其它类型的一个有效值。 这也意味着,你阻止不了将它们赋值给其它类型,就算是你想要阻止这种情况也不行。 null的发明者,Tony Hoare,称它为价值亿万美金的错误。
--strictNullChecks标记可以解决此错误:当你声明一个变量时,它不会自动地包含null或undefined。 你可以使用联合类型明确的包含它们:
注意,按照JavaScript的语义,TypeScript会把null和undefined区别对待。 string | null,string | undefined和string | undefined | null是不同的类型。
可选参数和可选属性
使用了--strictNullChecks,可选参数会被自动地加上| undefined:
可选属性也会有同样的处理:
类型守卫和类型断言
由于可以为null的类型是通过联合类型实现,那么你需要使用类型守卫来去除null。 幸运地是这与在JavaScript里写的代码一致:
这里很明显地去除了null,你也可以使用短路运算符:
如果编译器不能够去除null或undefined,你可以使用类型断言手动去除。 语法是添加!后缀:identifier!从identifier的类型里去除了null和undefined:
本例使用了嵌套函数,因为编译器无法去除嵌套函数的null(除非是立即调用的函数表达式)。 因为它无法跟踪所有对嵌套函数的调用,尤其是你将内层函数做为外层函数的返回值。 如果无法知道函数在哪里被调用,就无法知道调用时name的类型。
类型别名
类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。
起别名不会新建一个类型 - 它创建了一个新名字来引用那个类型。 给原始类型起别名通常没什么用,尽管可以做为文档的一种形式使用。
同接口一样,类型别名也可以是泛型 - 我们可以添加类型参数并且在别名声明的右侧传入:
我们也可以使用类型别名来在属性里引用自己:
与交叉类型一起使用,我们可以创建出一些十分稀奇古怪的类型。
然而,类型别名不能出现在声明右侧的任何地方。
接口 vs. 类型别名
像我们提到的,类型别名可以像接口一样;然而,仍有一些细微差别。
其一,接口创建了一个新的名字,可以在其它任何地方使用。 类型别名并不创建新名字—比如,错误信息就不会使用别名。 在下面的示例代码里,在编译器中将鼠标悬停在interfaced上,显示它返回的是Interface,但悬停在aliased上时,显示的却是对象字面量类型。
在旧版本的TypeScript里,类型别名不能被继承和实现(它们也不能继承和实现其它类型)。从TypeScript 2.7开始,类型别名可以被继承并生成新的交叉类型。例如:type Cat = Animal & { purrs: true }。
因为软件中的对象应该对于扩展是开放的,但是对于修改是封闭的,你应该尽量去使用接口代替类型别名。
另一方面,如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名。
字符串字面量类型
字符串字面量类型允许你指定字符串必须的固定值。 在实际应用中,字符串字面量类型可以与联合类型,类型守卫和类型别名很好的配合。 通过结合使用这些特性,你可以实现类似枚举类型的字符串。
你只能从三种允许的字符中选择其一来做为参数传递,传入其它值则会产生错误。
字符串字面量类型还可以用于区分函数重载:
数字字面量类型
TypeScript还具有数字字面量类型。
我们很少直接这样使用,但它们可以用在缩小范围调试bug的时候:
换句话说,当x与2进行比较的时候,它的值必须为1,这就意味着上面的比较检查是非法的。
枚举成员类型
如我们在枚举一节里提到的,当每个枚举成员都是用字面量初始化的时候枚举成员是具有类型的。
在我们谈及“单例类型”的时候,多数是指枚举成员类型和数字/字符串字面量类型,尽管大多数用户会互换使用“单例类型”和“字面量类型”。
可辨识联合(Discriminated Unions)
你可以合并单例类型,联合类型,类型守卫和类型别名来创建一个叫做可辨识联合的高级模式,它也称做标签联合或代数数据类型。 可辨识联合在函数式编程里很有用处。 一些语言会自动地为你辨识联合;而TypeScript则基于已有的JavaScript模式。 它具有3个要素:
具有普通的单例类型属性—可辨识的特征。
一个类型别名包含了那些类型的联合—联合。
此属性上的类型守卫。
首先我们声明了将要联合的接口。 每个接口都有kind属性但有不同的字符串字面量类型。 kind属性称做可辨识的特征或标签。 其它的属性则特定于各个接口。 注意,目前各个接口间是没有联系的。 下面我们把它们联合到一起:
现在我们使用可辨识联合:
完整性检查
当没有涵盖所有可辨识联合的变化时,我们想让编译器可以通知我们。 比如,如果我们添加了Triangle到Shape,我们同时还需要更新area:
有两种方式可以实现。 首先是启用--strictNullChecks并且指定一个返回值类型:
因为switch没有包含所有情况,所以TypeScript认为这个函数有时候会返回undefined。 如果你明确地指定了返回值类型为number,那么你会看到一个错误,因为实际上返回值的类型为number | undefined。 然而,这种方法存在些微妙之处且--strictNullChecks对旧代码支持不好。
第二种方法使用never类型,编译器用它来进行完整性检查:
这里,assertNever检查s是否为never类型—即为除去所有可能情况后剩下的类型。 如果你忘记了某个case,那么s将具有一个真实的类型并且你会得到一个错误。 这种方式需要你定义一个额外的函数,但是在你忘记某个case的时候也更加明显。
多态的this类型
this类型多态的this类型表示的是某个包含类或接口的子类型。 这被称做F-bounded多态性。 它能很容易的表现连贯接口间的继承,比如。 在计算器的例子里,在每个操作之后都返回this类型:
由于这个类使用了this类型,你可以继承它,新的类可以直接使用之前的方法,不需要做任何的改变。
如果没有this类型,ScientificCalculator就不能够在继承BasicCalculator的同时还保持接口的连贯性。 multiply将会返回BasicCalculator,它并没有sin方法。 然而,使用this类型,multiply会返回this,在这里就是ScientificCalculator。
索引类型(Index types)
使用索引类型,编译器就能够检查使用了动态属性名的代码。 例如,一个常见的JavaScript模式是从对象中选取属性的子集。
下面是如何在TypeScript里使用此函数,通过索引类型查询和索引访问操作符:
编译器会检查manufacturer和model是否真的是Car上的一个属性。 本例还引入了几个新的类型操作符。 首先是keyof T,索引类型查询操作符。 对于任何类型T,keyof T的结果为T上已知的公共属性名的联合。 例如:
keyof Car是完全可以与'manufacturer' | 'model' | 'year'互相替换的。 不同的是如果你添加了其它的属性到Car,例如ownersAddress: string,那么keyof Car会自动变为'manufacturer' | 'model' | 'year' | 'ownersAddress'。 你可以在像pluck函数这类上下文里使用keyof,因为在使用之前你并不清楚可能出现的属性名。 但编译器会检查你是否传入了正确的属性名给pluck:
第二个操作符是T[K],索引访问操作符。 在这里,类型语法反映了表达式语法。 这意味着person['name']具有类型Person['name'] — 在我们的例子里则为string类型。 然而,就像索引类型查询一样,你可以在普通的上下文里使用T[K],这正是它的强大所在。 你只要确保类型变量K extends keyof T就可以了。 例如下面getProperty函数的例子:
getProperty里的o: T和propertyName: K,意味着o[propertyName]: T[K]。 当你返回T[K]的结果,编译器会实例化键的真实类型,因此getProperty的返回值类型会随着你需要的属性改变。
索引类型和字符串索引签名
keyof和T[K]与字符串索引签名进行交互。索引签名的参数类型必须为number或string。 如果你有一个带有字符串索引签名的类型,那么keyof T会是string | number。 (并非只有string,因为在JavaScript里,你可以使用字符串object['42'或 数字object[42]索引来访问对象属性)。 并且T[string]为索引签名的类型:
如果一个类型带有数字索引签名,那么keyof T为number。
映射类型
一个常见的任务是将一个已知的类型每个属性都变为可选的:
或者我们想要一个只读版本:
这在JavaScript里经常出现,TypeScript提供了从旧类型中创建新类型的一种方式 — 映射类型。 在映射类型里,新类型以相同的形式去转换旧类型里每个属性。 例如,你可以令每个属性成为readonly类型或可选的。 下面是一些例子:
像下面这样使用:
需要注意的是这个语法描述的是类型而非成员。 若想添加成员,则可以使用交叉类型:
下面来看看最简单的映射类型和它的组成部分:
它的语法与索引签名的语法类型,内部使用了for .. in。 具有三个部分:
类型变量
K,它会依次绑定到每个属性。字符串字面量联合的
Keys,它包含了要迭代的属性名的集合。属性的结果类型。
在个简单的例子里,Keys是硬编码的属性名列表并且属性类型永远是boolean,因此这个映射类型等同于:
在真正的应用里,可能不同于上面的Readonly或Partial。 它们会基于一些已存在的类型,且按照一定的方式转换字段。 这就是keyof和索引访问类型要做的事情:
但它更有用的地方是可以有一些通用版本。
在这些例子里,属性列表是keyof T且结果类型是T[P]的变体。 这是使用通用映射类型的一个好模版。 因为这类转换是同态的,映射只作用于T的属性而没有其它的。 编译器知道在添加任何新属性之前可以拷贝所有存在的属性修饰符。 例如,假设Person.name是只读的,那么Partial<Person>.name也将是只读的且为可选的。
下面是另一个例子,T[P]被包装在Proxy<T>类里:
注意Readonly<T>和Partial<T>用处不小,因此它们与Pick和Record一同被包含进了TypeScript的标准库里:
Readonly,Partial和Pick是同态的,但Record不是。 因为Record并不需要输入类型来拷贝属性,所以它不属于同态:
非同态类型本质上会创建新的属性,因此它们不会从它处拷贝属性修饰符。
由映射类型进行推断
现在你了解了如何包装一个类型的属性,那么接下来就是如何拆包。 其实这也非常容易:
注意这个拆包推断只适用于同态的映射类型。 如果映射类型不是同态的,那么需要给拆包函数一个明确的类型参数。
有条件类型
TypeScript 2.8引入了有条件类型,它能够表示非统一的类型。 有条件的类型会以一个条件表达式进行类型关系检测,从而在两种类型中选择其一:
上面的类型意思是,若T能够赋值给U,那么类型是X,否则为Y。
有条件的类型T extends U ? X : Y或者解析为X,或者解析为Y,再或者延迟解析,因为它可能依赖一个或多个类型变量。 若T或U包含类型参数,那么是否解析为X或Y或推迟,取决于类型系统是否有足够的信息来确定T总是可以赋值给U。
下面是一些类型可以被立即解析的例子:
另外一个例子涉及TypeName类型别名,它使用了嵌套了有条件类型:
下面是一个有条件类型被推迟解析的例子:
这里,a变量含有未确定的有条件类型。 当有另一段代码调用foo,它会用其它类型替换U,TypeScript将重新计算有条件类型,决定它是否可以选择一个分支。
与此同时,我们可以将有条件类型赋值给其它类型,只要有条件类型的每个分支都可以赋值给目标类型。 因此在我们的例子里,我们可以将U extends Foo ? string : number赋值给string | number,因为不管这个有条件类型最终结果是什么,它只能是string或number。
分布式有条件类型
如果有条件类型里待检查的类型是naked type parameter,那么它也被称为“分布式有条件类型”。 分布式有条件类型在实例化时会自动分发成联合类型。 例如,实例化T extends U ? X : Y,T的类型为A | B | C,会被解析为(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)。
例子
在T extends U ? X : Y的实例化里,对T的引用被解析为联合类型的一部分(比如,T指向某一单个部分,在有条件类型分布到联合类型之后)。 此外,在X内对T的引用有一个附加的类型参数约束U(例如,T被当成在X内可赋值给U)。
例子
注意在Boxed<T>的true分支里,T有个额外的约束any[],因此它适用于T[number]数组元素类型。同时也注意一下有条件类型是如何分布成联合类型的。
有条件类型的分布式的属性可以方便地用来过滤联合类型:
有条件类型与映射类型结合时特别有用:
与联合类型和交叉类型相似,有条件类型不允许递归地引用自己。比如下面的错误。
例子
有条件类型中的类型推断
现在在有条件类型的extends子语句中,允许出现infer声明,它会引入一个待推断的类型变量。 这个推断的类型变量可以在有条件类型的true分支中被引用。 允许出现多个同类型变量的infer。
例如,下面代码会提取函数类型的返回值类型:
有条件类型可以嵌套来构成一系列的匹配模式,按顺序进行求值:
下面的例子解释了在协变位置上,同一个类型变量的多个候选类型会被推断为联合类型:
相似地,在抗变位置上,同一个类型变量的多个候选类型会被推断为交叉类型:
当推断具有多个调用签名(例如函数重载类型)的类型时,用最后的签名(大概是最自由的包含所有情况的签名)进行推断。 无法根据参数类型列表来解析重载。
无法在正常类型参数的约束子语句中使用infer声明:
但是,可以这样达到同样的效果,在约束里删掉类型变量,用有条件类型替换:
预定义的有条件类型
TypeScript 2.8在lib.d.ts里增加了一些预定义的有条件类型:
Exclude<T, U>-- 从T中剔除可以赋值给U的类型。Extract<T, U>-- 提取T中可以赋值给U的类型。NonNullable<T>-- 从T中剔除null和undefined。ReturnType<T>-- 获取函数返回值类型。InstanceType<T>-- 获取构造函数类型的实例类型。
Example
注意:
Exclude类型是建议的Diff类型的一种实现。我们使用Exclude这个名字是为了避免破坏已经定义了Diff的代码,并且我们感觉这个名字能更好地表达类型的语义。
Last updated
Was this helpful?