高级类型
交集类型
交集类型(Intersection Type)将多个类型合并为一个类型。这种方式允许你把现有的所有类型合并到一个类型中,且该类型有你需要的所有功能。例如,Person & Serializable & Loggable 是 Person,也是 Serializable,还是 Loggable。这意味着这个类型的实例将拥有三种类型的所有成员。
你通常会在 mixin 类型以及面向对象模型不适合的地方看到交集类型的使用(JavaScript 中有很多这种功能!)。下面是一个 mixin 的列子。
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);
联合类型
联合类型和上面提到的交集类型有点类似,但它们的用法完全不一样。有时候你会遇到一个期望参数既可以是数字又可以是字符串的库。例如,下面这个函数:
/**
* Takes a string and adds "padding" to the left.
* If 'padding' is a string, then 'padding' is appended to the left side.
* If 'padding' is a number, then that number of spaces is added to the left side.
*/
function padLeft(value: string, padding: any) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(`Expected string or number, got '${padding}'.`);
}
padLeft("Hello world", 4); // returns " Hello world"
padLeft 函数的问题在于 padding 参数的类型是 any,也就是说我们在调用函数时为 padding 设置的值可以既不是 number 也不是 string,但是 TypeScript 认为代码是没有问题的。
let indentedString = padLeft("Hello world", true); // passes at compile time, fails at runtime.
在传统的面向对象代码中,我们可以通过创建类型的层次结构来抽象化这两种类型。虽然这种方式更为明确,但也有点小题大做了。padLeft 原始版本中我们可以传递基础类型,这是好事。这也就意味着函数的用法简洁明了。如果已经有其他函数解决这个问题,那下面说的新方法也就没多大用处了。
与使用 any 不同,我们将 padding 参数声明为联合类型。
/**
* Takes a string and adds "padding" to the left.
* If 'padding' is a string, then 'padding' is appended to the left side.
* If 'padding' is a number, then that number of spaces is added to the left side.
*/
function padLeft(value: string, padding: string | number) {
// ...
}
let indentedString = padLeft("Hello world", true); // errors during compilation
联合类型表示声明的变量或传递的参数的类型可以是多个类型中的任意一个,我们可以使用 | 来将每个类型分开,所以类型 number | string | boolean 的值的类型可以是三种类型中的任意一种。
如果我们有一个联合类型的值,我们只能访问在联合体中对所有类型都通用的属性。
interface Bird {
fly();
layEggs();
}
interface Fish {
swim();
layEggs();
}
function getSmallPet(): Fish | Bird {
// ...
}
let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim(); // errors
联合类型这样看起来会有些麻烦,但习惯就好了。如果 a 的类型是 A | B,我们只能确认 a 具有 A 和 B 都有的成员。在这个例子中,Bird 有 fly 成员,但我们不能确认联合类型 Bird | Fish 就会有 fly 方法。如果在运行时变量的类型是 Fish,那我们调用 pet.fly() 就会失败。
类型保护和区分类型
联合类型对在建模时值可以互相重叠的情形非常有用,当我们需要明确知道我们是否有一个 Fish 类型的变量时会发生什么呢?JavaScript 中区分两个可能值的常见用法是检查成员是否存在。正如我们提到的,你只能访问联合类型的所有类型都具有的成员属性。
let pet = getSmallPet();
// Each of these property accesses will cause an error
if (pet.swim) {
pet.swim();
}
else if (pet.fly) {
pet.fly();
}
为了使以上代码正常工作,我们需要使用类型断言。
let pet = getSmallPet();
if ((pet as Fish).swim) {
(pet as Fish).swim();
} else if ((pet as Bird).fly) {
(pet as Bird).fly();
}