这是我参与11月更文挑战的第 5 天,活动详情查看:2021最后一次更文挑战
前面我们学习了Typescript
中的基础类型、泛型和接口等,实现了对变量、函数参数、函数返回值等的静态类型检查。对于较为复杂的情况,我们可以借助交叉类型、联合类型、类型保护等高级类型去实现我们的需求。
交叉类型(Intersection Types)
交叉类型是将多个类型合并为一个类型,包含了这些类型的所有特性。我们大多是在混入(mixins)或其它不适合典型面向对象模型的地方看到交叉类型的使用。下面是如何创建混入的一个简单例子:
function extend<T, U>(first: T, second: U): T & U {
let result = <T & U>{}; // 包含T和U的所有属性
for (let id in first) {
(<any>result)[id] = (<any>first)[id];
}
for (let id in second) {
if (!result.hasOwnProperty(id)) {
(<any>result)[id] = (<any>second)[id];
}
}
return result;
}
class Person {
constructor(public name: string) { }
}
interface Loggable {
log(): void;
}
class ConsoleLogger implements Loggable {
log() {}
}
var jim = extend(new Person("Jim"), new ConsoleLogger()); // 拥有name属性和log()方法
var n = jim.name;
jim.log();
联合类型(Union Types)
联合类型表示一个值可以是几种类型之一。 我们用竖线( |
)分隔每个类型,所以 number | string | boolean
表示一个值可以是 number
, string
,或 boolean
。
如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function getSmallPet(): Fish | Bird {
let obj: Bird = {
fly() {},
layEggs() {}
}
return obj
}
let pet = getSmallPet();
pet.layEggs(); // okay
// pet.swim(); // Property 'swim' does not exist on type 'Bird | Fish'.
类型保护与区分类型(Type Guards and Differentiating Types)
自定义类型保护
对于一个联合类型的值,我们只能访问联合类型中共有的成员。上面的例子中,为了能访问pet.swim()
,我们可以使用类型断言
:
let pet = getSmallPet();
if ((<Fish>pet).swim) {
(<Fish>pet).swim();
} else {
(<Bird>pet).fly();
}
除了使用类型断言,我们还能借助类型保护
去实现。类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。要定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个类型谓词
:
let pet = getSmallPet();
function isFish(pet: Fish | Bird): pet is Fish {
return (<Fish>pet).swim !== undefined;
}
// 'swim' 和 'fly' 调用都没有问题了
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
这里的pet is Fish
就是类型谓词,是parameterName is Type
的形式。
typeof类型保护
typeof类型保护
只有两种形式能被识别: typeof v === "typename"
和 typeof v !== "typename"
,其中"typename"
必须是 "number"
, "string"
, "boolean"
或 "symbol"
。但是TypeScript并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。
为什么没有其他的"typename"
呢?因为对于数组或者null
,typeof v === "object"
也返回true。因此Typescript只将这四个"typename"
识别为类型保护。
function padLeft(value: string, padding: string | number) {
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}'.`);
}
instanceof类型保护
interface Padder {
getPaddingString(): string
}
class SpaceRepeatingPadder implements Padder {
constructor(private numSpaces: number) { }
getPaddingString() {
return Array(this.numSpaces + 1).join(" ");
}
}
class StringPadder implements Padder {
constructor(private value: string) { }
getPaddingString() {
return this.value;
}
}
function getRandomPadder() {
return Math.random() < 0.5 ? new SpaceRepeatingPadder(4) : new StringPadder(" ");
}
let padder: Padder = getRandomPadder(); // 类型为 SpaceRepeatingPadder | StringPadder
if (padder instanceof SpaceRepeatingPadder) {
padder; // 类型细化为'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
padder; // 类型细化为'StringPadder'
}
可以为null
与undefined
的类型
默认情况下,类型检查器认为 null
与 undefined
可以赋值给任何类型。 null
与 undefined
是所有其它类型的一个有效值。这也意味着,你阻止不了将它们赋值给其它类型。
--strictNullChecks
标记可以解决此错误:当你声明一个变量时,它不会自动地包含 null
或 undefined
。 你可以使用联合类型明确的包含它们:
let s = "foo";
s = null; // 错误, 'null'不能赋值给'string'
let sn: string | null = "bar";
sn = null; // 可以
sn = undefined; // error, 'undefined'不能赋值给'string | null'
可选参数和可选属性
使用了 --strictNullChecks
,可选参数会被自动地加上 | undefined
:
function f(x: number, y?: number) {
return x + (y || 0);
}
f(1, 2);
f(1);
f(1, undefined);
f(1, null); // error, 'null' is not assignable to 'number | undefined'
可选属性也会有同样的处理:
class C {
a: number;
b?: number;
}
let c = new C();
c.a = 12;
c.a = undefined; // error, 'undefined' is not assignable to 'number'
c.b = 13;
c.b = undefined; // ok
c.b = null; // error, 'null' is not assignable to 'number | undefined'
类型保护和类型断言
由于可以为null的类型是通过联合类型实现,那么你需要使用类型保护或短路运算符来去除 null
,这与在JavaScript里写的代码一致:
// 使用类型保护
function f(sn: string | null): string {
if (sn == null) {
return "default";
} else {
return sn;
}
}
// 使用短路运算符
function f(sn: string | null): string {
return sn || "default";
}
如果编译器不能够去除 null
或 undefined
,你可以使用类型断言
手动去除。 语法是添加!
后缀: identifier!
从 identifier
的类型里去除了 null
和 undefined
:
function broken(name: string | null): string {
function postfix(epithet: string) {
return name.charAt(0) + '. the ' + epithet; // error, 'name' is possibly null
}
name = name || "Bob";
return postfix("great");
}
function fixed(name: string | null): string {
function postfix(epithet: string) {
return name!.charAt(0) + '. the ' + epithet; // ok
}
name = name || "Bob";
return postfix("great");
}
类型别名type
类型别名会给一个类型起个新名字,我们可以使用 type
创建类型别名。起别名不会新建一个类型,而是创建了原类型的一个引用。
类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
同接口一样,类型别名也可以是泛型,我们可以添加类型参数并且在别名声明的右侧传入:
type Container<T> = { value: T };
我们也可以使用类型别名来在属性里引用自己:
type Tree<T> = {
value: T;
left: Tree<T>;
right: Tree<T>;
}
与交叉类型一起使用,我们可以创建出一些十分稀奇古怪的类型。
type LinkedList<T> = T & { next: LinkedList<T> };
interface Person {
name: string;
}
// 加!就不提示:Variable 'people' is used before being assigned.
var people!: LinkedList<Person>;
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;
然而,类型别名不能出现在声明右侧的任何地方。
type Yikes = Array<Yikes>; // error
接口 vs. 类型别名
像我们提到的,类型别名可以像接口一样;然而,仍有一些细微差别。
其一,接口创建了一个新的名字,可以在其它任何地方使用。而类型别名并不创建新名字。比如,错误信息就不会使用别名。在下面的示例代码里,在编译器中将鼠标悬停在 interfaced
上,显示它返回的是 Interface
,但悬停在 aliased
上时,显示的却是对象字面量类型。
type Alias = { num: number }
interface Interface {
num: number;
}
declare function aliased(arg: Alias): Alias;
declare function interfaced(arg: Interface): Interface;
其二,类型别名不能被 extends
和 implements
(自己也不能 extends
和 implements
其它类型)。因为软件中的对象应该对于扩展是开放的,但是对于修改是封闭的,你应该尽量去使用接口代替类型别名。
其三,如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名。