高级类型

交叉类型(Intersection Types)

交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。 例如, Person & Serializable & Loggable同时是 Person 和 Serializable 和 Loggable。 就是说这个类型的对象同时拥有了这三种类型的成员。

我们大多是在混入(mixins)或其它不适合典型面向对象模型的地方看到交叉类型的使用。 (在JavaScript里发生这种情况的场合很多!) 下面是如何创建混入的一个简单例子:

  1. function extend<T, U>(first: T, second: U): T & U {
  2. let result = <T & U>{};
  3. for (let id in first) {
  4. (<any>result)[id] = (<any>first)[id];
  5. }
  6. for (let id in second) {
  7. if (!result.hasOwnProperty(id)) {
  8. (<any>result)[id] = (<any>second)[id];
  9. }
  10. }
  11. return result;
  12. }
  13. class Person {
  14. constructor(public name: string) { }
  15. }
  16. interface Loggable {
  17. log(): void;
  18. }
  19. class ConsoleLogger implements Loggable {
  20. log() {
  21. // ...
  22. }
  23. }
  24. var jim = extend(new Person("Jim"), new ConsoleLogger());
  25. var n = jim.name;
  26. jim.log();

联合类型(Union Types)

需求:一个代码库希望传入 number或 string类型的参数。

  1. /**
  2. * Takes a string and adds "padding" to the left.
  3. * If 'padding' is a string, then 'padding' is appended to the left side.
  4. * If 'padding' is a number, then that number of spaces is added to the left side.
  5. */
  6. function padLeft(value: string, padding: any) {
  7. if (typeof padding === "number") {
  8. return Array(padding + 1).join(" ") + value;
  9. }
  10. if (typeof padding === "string") {
  11. return padding + value;
  12. }
  13. throw new Error(`Expected string or number, got '${padding}'.`);
  14. }
  15. padLeft("Hello world", 4); // returns " Hello world"

padLeft存在一个问题, padding参数的类型指定成了 any。 这就是说我们可以传入一个既不是 number也不是 string类型的参数,但是TypeScript却不报错。

传统的面向对象语言,会将这两种类型抽象成有层级的类型,清晰但是过渡设计。我们可以使用联合类型解决这个问题

  1. /**
  2. * Takes a string and adds "padding" to the left.
  3. * If 'padding' is a string, then 'padding' is appended to the left side.
  4. * If 'padding' is a number, then that number of spaces is added to the left side.
  5. */
  6. function padLeft(value: string, padding: string | number) {
  7. // ...
  8. }
  9. let indentedString = padLeft("Hello world", true); // errors during compilation

联合类型表示一个值可以是几种类型之一。使用(|)分隔每个类型

如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。

  1. interface Bird {
  2. fly();
  3. layEggs();
  4. }
  5. interface Fish {
  6. swim();
  7. layEggs();
  8. }
  9. function getSmallPet(): Fish | Bird {
  10. // ...
  11. }
  12. let pet = getSmallPet();
  13. pet.layEggs(); // okay
  14. pet.swim(); // errors

类型保护与区分类型(Type Guards and Differentiating Types)

我们需要确切了解是否为Fish时,JavaScript里常用来区分2个可能值的方法是检查成员是否存在。

  1. let pet = getSmallPet();
  2. // 每一个成员访问都会报错
  3. if (pet.swim) {
  4. pet.swim();
  5. }
  6. else if (pet.fly) {
  7. pet.fly();
  8. }

需要使用类型断言

  1. let pet = getSmallPet();
  2. if ((<Fish>pet).swim) {
  3. (<Fish>pet).swim();
  4. }
  5. else {
  6. (<Bird>pet).fly();
  7. }

用户自定义的类型保护

上面的情况需要多次使用类型断言。我们希望只检查一次。typeScript的类型保护机制可以实现。会在运行检查时确保在某个作用域里的类型。定义类型保护,我们需要定义一个函数,其返回值是一个类型谓词:

  1. function isFish(pet: Fish | Bird): pet is Fish {
  2. return (<Fish>pet).swim !== undefined;
  3. }

在这个例子里, pet is Fish就是类型谓词。 谓词为 parameterName is Type这种形式, parameterName必须是来自于当前函数签名里的一个参数名。

每当使用一些变量调用 isFish时,TypeScript会将变量缩减为那个具体的类型,只要这个类型与变量的原始类型是兼容的。

  1. // 'swim' 和 'fly' 调用都没有问题了
  2. if (isFish(pet)) {
  3. pet.swim();
  4. }
  5. else {
  6. pet.fly();
  7. }

注意TypeScript不仅知道在 if分支里 pet是 Fish类型; 它还清楚在 else分支里,一定 不是 Fish类型,一定是 Bird类型。

typeof类型保护

上面的缺点是,必须要定义一个函数来判断类型是否是原始类型。不过TypeScript可以将typeof识别为一个类型保护。我们可以直接在代码里检查类型了。

  1. function padLeft(value: string, padding: string | number) {
  2. if (typeof padding === "number") {
  3. return Array(padding + 1).join(" ") + value;
  4. }
  5. if (typeof padding === "string") {
  6. return padding + value;
  7. }
  8. throw new Error(`Expected string or number, got '${padding}'.`);
  9. }

这些 typeof类型保护只有两种形式能被识别: typeof v === “typename”和 typeof v !== “typename”, “typename”必须是 “number”, “string”, “boolean”或 “symbol”。 但是TypeScript并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。

instanceof类型保护

instanceof类型保护是通过构造函数来细化类型的一种方式

instanceof的右侧要求是一个构造函数,TypeScript将细化为:

此构造函数的 prototype属性的类型,如果它的类型不为 any的话
构造签名所返回的类型的联合

  1. interface Padder {
  2. getPaddingString(): string
  3. }
  4. class SpaceRepeatingPadder implements Padder {
  5. constructor(private numSpaces: number) { }
  6. getPaddingString() {
  7. return Array(this.numSpaces + 1).join(" ");
  8. }
  9. }
  10. class StringPadder implements Padder {
  11. constructor(private value: string) { }
  12. getPaddingString() {
  13. return this.value;
  14. }
  15. }
  16. function getRandomPadder() {
  17. return Math.random() < 0.5 ?
  18. new SpaceRepeatingPadder(4) :
  19. new StringPadder(" ");
  20. }
  21. // 类型为SpaceRepeatingPadder | StringPadder
  22. let padder: Padder = getRandomPadder();
  23. if (padder instanceof SpaceRepeatingPadder) {
  24. padder; // 类型细化为'SpaceRepeatingPadder'
  25. }
  26. if (padder instanceof StringPadder) {
  27. padder; // 类型细化为'StringPadder'
  28. }

可以为null的类型

null和 undefined是两种特殊类型,类型检查器认为 null与undefined可以赋值给任何类型。null与 undefined是所有其它类型的一个有效值。

—strictNullChecks标记可以解决此错误:当你声明一个变量时,它不会自动地包含 null或 undefined 可以使用联合类型明确的包含它们:

  1. let s = "foo";
  2. s = null; // 错误, 'null'不能赋值给'string'
  3. let sn: string | null = "bar";
  4. sn = null; // 可以
  5. sn = undefined; // error, 'undefined'不能赋值给'string | null'

注意,按照JavaScript的语义,TypeScript会把 null和 undefined区别对待。 string | null, string | undefined和 string | undefined | null是不同的类型。

可选参数和可选属性

使用了 —strictNullChecks,可选参数会被自动地加上 | undefined:

  1. function f(x: number, y?: number) {
  2. return x + (y || 0);
  3. }
  4. f(1, 2);
  5. f(1);
  6. f(1, undefined);
  7. f(1, null); // error, 'null' is not assignable to 'number | undefined'

可选属性也会有同样的处理:

  1. class C {
  2. a: number;
  3. b?: number;
  4. }
  5. let c = new C();
  6. c.a = 12;
  7. c.a = undefined; // error, 'undefined' is not assignable to 'number'
  8. c.b = 13;
  9. c.b = undefined; // ok
  10. c.b = null; // error, 'null' is not assignable to 'number | undefined'

类型保护和类型断言

由于可以为null的类型是通过联合类型实现,那么你需要使用类型保护来去除 null。 幸运地是这与在JavaScript里写的代码一致:

  1. function f(sn: string | null): string {
  2. if (sn == null) {
  3. return "default";
  4. }
  5. else {
  6. return sn;
  7. }
  8. }

这里很明显地去除了 null,你也可以使用短路运算符:

  1. function f(sn: string | null): string {
  2. return sn || "default";
  3. }

如果编译器不能够去除 null或 undefined,你可以使用类型断言手动去除。 语法是添加 !后缀: identifier!从 identifier的类型里去除了 null和 undefined:

  1. function broken(name: string | null): string {
  2. function postfix(epithet: string) {
  3. return name.charAt(0) + '. the ' + epithet; // error, 'name' is possibly null
  4. }
  5. name = name || "Bob";
  6. return postfix("great");
  7. }
  8. function fixed(name: string | null): string {
  9. function postfix(epithet: string) {
  10. return name!.charAt(0) + '. the ' + epithet; // ok
  11. }
  12. name = name || "Bob";
  13. return postfix("great");
  14. }

本例使用了嵌套函数,因为编译器无法去除嵌套函数的null(除非是立即调用的函数表达式)。 因为它无法跟踪所有对嵌套函数的调用,尤其是你将内层函数做为外层函数的返回值。 如果无法知道函数在哪里被调用,就无法知道调用时 name的类型。

类型别名

类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。同接口一样,类型别名也可以是泛型 - 我们可以添加类型参数并且在别名声明的右侧传入

  1. type Name=string
  2. type Container<T>={value:T};
  3. // 引用自己
  4. type Tree<T> = {
  5. value: T;
  6. left: Tree<T>;
  7. right: Tree<T>;
  8. }

与交叉类型一起使用

  1. type LinkedList<T> = T & { next: LinkedList<T> };
  2. interface Person {
  3. name: string;
  4. }
  5. var people: LinkedList<Person>;
  6. var s = people.name;
  7. var s = people.next.name;
  8. var s = people.next.next.name;
  9. var s = people.next.next.next.name;

类型别名不能出现在声明右侧的任何地方。

  1. type Yikes = Array<Yikes>; // error

接口 vs 类型别名

  • 接口创建了一个新的名字,可以在其他任何地方使用。类型别名并不创建新名字。
  • 类型别名不能被extends湖人implements(自己也不可以extend和implements其他类型),因为软件中的对象对于扩展开放,对于修改封闭。尽量用接口去替代类型别名。

如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名。

字符字面量类型

字符串字面量类型允许你指定字符串必须的固定值。字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合。 通过结合使用这些特性,可以实现类似枚举类型的字符串。

  1. type Easing = "ease-in" | "ease-out" | "ease-in-out";
  2. class UIElement {
  3. animate(dx: number, dy: number, easing: Easing) {
  4. if (easing === "ease-in") {
  5. // ...
  6. }
  7. else if (easing === "ease-out") {
  8. }
  9. else if (easing === "ease-in-out") {
  10. }
  11. else {
  12. // error! should not pass null or undefined.
  13. }
  14. }
  15. }
  16. let button = new UIElement();
  17. button.animate(0, 0, "ease-in");
  18. button.animate(0, 0, "uneasy"); // error: "uneasy" is not allowed here

只能从三种允许的字符中选择其一来做为参数传递,传入其它值则会产生错误。

字符串字面量类型还可以用于区分函数重载

  1. function createElement(tagName: "img"): HTMLImageElement;
  2. function createElement(tagName: "input"): HTMLInputElement;
  3. // ... more overloads ...
  4. function createElement(tagName: string): Element {
  5. // ... code goes here ...
  6. }

数字字面量类型

  1. function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 {
  2. // ...
  3. }
  1. function foo(x: number) {
  2. if (x !== 1 || x !== 2) {
  3. // ~~~~~~~
  4. // Operator '!==' cannot be applied to types '1' and '2'.
  5. }
  6. }

当 x与 2进行比较的时候,它的值必须为 1,这就意味着上面的比较检查是非法的。

枚举成员类型

每个枚举成员都是用字面量初始化的时候枚举成员是具有类型的。

“单例类型”,多数是指枚举成员类型和数字/字符串字面量类型,尽管大多数用户会互换使用“单例类型”和“字面量类型”。

可辨识联合(Discriminated Unions)

你可以合并单例类型,联合类型,类型保护和类型别名来创建一个叫做 可辨识联合的高级模式,它也称做 标签联合或 代数数据类型。 可辨识联合在函数式编程很有用处。 一些语言会自动地为你辨识联合;而TypeScript则基于已有的JavaScript模式。 它具有3个要素:

具有普通的单例类型属性— 可辨识的特征。
一个类型别名包含了那些类型的联合— 联合。
此属性上的类型保护。

  1. interface Square {
  2. kind: "square";
  3. size: number;
  4. }
  5. interface Rectangle {
  6. kind: "rectangle";
  7. width: number;
  8. height: number;
  9. }
  10. interface Circle {
  11. kind: "circle";
  12. radius: number;
  13. }
  1. type Shape = Square | Rectangle | Circle;

使用可辨识联合

  1. function area(s: Shape) {
  2. switch (s.kind) {
  3. case "square": return s.size * s.size;
  4. case "rectangle": return s.height * s.width;
  5. case "circle": return Math.PI * s.radius ** 2;
  6. }
  7. }

完整性检查
没有涵盖所有可辨识联合的变化时,可以让编辑器通知我们。
有两种方式实现

第一种 启用—strictNullChecks并且指定一个返回值类型:

  1. function area(s: Shape): number { // error: returns number | undefined
  2. switch (s.kind) {
  3. case "square": return s.size * s.size;
  4. case "rectangle": return s.height * s.width;
  5. case "circle": return Math.PI * s.radius ** 2;
  6. }
  7. }

switch未包含所有情况,导致有时候会返回undefined。明确返回值类型。就会看到错误。缺点是对旧代码支持不好

第二种方法使用never类型

  1. function assertNever(x: never): never {
  2. throw new Error("Unexpected object: " + x);
  3. }
  4. function area(s: Shape) {
  5. switch (s.kind) {
  6. case "square": return s.size * s.size;
  7. case "rectangle": return s.height * s.width;
  8. case "circle": return Math.PI * s.radius ** 2;
  9. default: return assertNever(s); // error here if there are missing cases
  10. }
  11. }

这种方式需要你定义一个额外的函数,但是在你忘记某个case的时候也更加明显。

多态的this类型

多态的 this类型表示的是某个包含类或接口的 子类型。 这被称做 F-bounded多态性。 它能很容易的表现连贯接口间的继承,比如。 在计算器的例子里,在每个操作之后都返回 this类型:

索引类型(Index types)

使用索引类型,编译器就能够检查使用了动态属性名的代码。例如,一个常见的JavaScript模式是从对象中选取属性的子集。

部分待完善。