TypeScript 高级类型
⒈交叉类型(Intersection Types)
交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。 例如, Person & Serializable & Loggable
同时是 Person
和 Serializable
和 Loggable
。 就是说这个类型的对象同时拥有了这三种类型的成员。
每当我们正确的使用交叉类型的时候,TypeScript可以帮我们合理地将两个不同类型叠加为新的类型,并包含了所需的所有类型。
我们大多是在混入(mixins)或其它不适合典型面向对象模型的地方看到交叉类型的使用。 (在JavaScript里发生这种情况的场合很多!)
type newType = number & string; let a : newType; interface A{ a:number, b:string, } interface B{ c:string, d:string, } type newType2 = A & B; let b : newType2;
这里的Type关键字是用来声明类型变量的。在运行时,与类型相关的代码都会被移除掉,并不会影响到JavaScript的执行。
*当交叉类型中有属性冲突时,则无论如何赋值都不可能通过类型检查。如下面的代码所示:
interface A{ a:number, b:string, } interface B{ c:string, d:string, } type newType = A & B; let a : newType = {a:1,b:'',c:'',d:''}; a.a = 1; a.b = ''; a.c = ''; a.d = 5; //Error,无法通过类型检查
⒉联合类型(Union Types)
联合类型与交叉类型类似,但使用上却完全不同。
例如我们需要一个变量可能是number,也有可能是string,这是一个很常见的场景,联合类型便是用于解决这样的问题。
比如下面这段经典的函数:
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
。 也就是说,我们可以传入一个既不是 number
也不是 string
类型的参数,但是TypeScript却不报错。
let indentedString = padLeft("Hello world", true); // 编译阶段通过,运行时报错
在传统的面向对象语言里,我们可以使用重载或将这两种类型抽象成有层级的类型(父类与子类)。 这么做显然是非常清晰的,但同时也存在了过度设计。
因为在JavaScript中并没有重载可以使用(可以使用特殊的方式创建出类似重载的函数),因此在JavaScript的函数中手动去判断参数的类型这种操作更为常见,这在一定程度上避免了过度设计。
padLeft
原始版本的好处之一是允许我们传入原始类型。 这样做的话使用起来既简单又方便。 如果我们就是想使用已经存在的函数的话,这种新的方式就不适用了。
如果我们希望更准确的描述padding的类型,就可以使用联合类型将padding的类型限定为既可以是number又可以是string。
代替 any
, 我们可以使用 联合类型作为 padding
的参数:
function padLeft(value: string, padding: string | number) { // ... } let indentedString = padLeft("Hello world", true); // 编译器报错,类型true的参数不能赋值给类型string|number的参数
联合类型表示一个变量可以是几种类型之一。 我们用竖线( |
)分隔每个类型,所以 number | string | boolean
表示一个值可以是 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 | B
,我们能够 确定的是它包含了 A
和 B
中共有的成员。 这个例子里, Bird
具有一个 fly
成员。 我们不能确定一个 Bird | Fish
类型的变量是否有 fly
方法。 如果变量在运行时是 Fish
类型,那么调用 pet.fly()
就出错了。
联合类型取的是交集,交叉类型取的是并集,这听起来和它们的名字有些冲突。
**谨记,TypeScript只会帮你在编译时做类型检查,并不确保你的代码在运行过程中的安全。
⒊类型保护【区分值的类型】
联合类型适合于那些值可以为不同类型的情况。 但当我们想确切地了解某个值的类型时该怎么办? JavaScript里常用来区分2个可能值的方法是检查成员是否存在。 如之前提及的,我们只能访问联合类型中共同拥有的成员。
let pet = getSmallPet(); // 每一个成员访问都会报错 if (pet.swim) { pet.swim(); } else if (pet.fly) { pet.fly(); }
而在TypeScript中,我们可以使用类型断言。
let pet = getSmallPet(); if ((<Fish>pet).swim) { (<Fish>pet).swim(); } else { (<Bird>pet).fly(); }
为了准确判断值的类型,我们在方法体中多次使用了类型断言(即使通过了类型断言,我们知道了值的类型,在接下来的代码中,我们仍然要对其添加类型断言),这是一件非常麻烦的事情。如果我们一旦检查并确定了值的类型,在之后的代码中无需类型断言就能清楚地知道值的类型的话就好了。
1.自定义类型保护(用户自定义的类型保护)
TypeScript里的 类型保护机制让它成为了现实。 类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。 既使可读性得到提升,又减少了使用烦琐的类型断言。要定义一个类型保护,我们只需要简单地定义一个函数就可以,但返回值是一个主谓宾语句( 类型谓词),如下所示:
function isFish(pet: Fish | Bird): pet is Fish { return (<Fish>pet).swim !== undefined; }
在这个例子里, pet is Fish
就是类型谓词。 谓词为 parameterName is Type
这种形式, parameterName
必须是来自于当前函数签名里的一个参数名。
每当使用一些变量调用 isFish
时,TypeScript会将变量指定为类型保护中的类型,只要这个类型与变量的原始类型是兼容的。
// 'swim' 和 'fly' 调用都没有问题了 if (isFish(pet)) { pet.swim(); } else { pet.fly(); }
注意,TypeScript不仅知道在 if
分支里 pet
是 Fish
类型; 它还清楚在 else
分支里pet一定是 Bird
类型,这得益于类型保护的实现。
2.typeof
类型保护
现在我们可以使用类型保护来重构一开始的padLeft代码了,可以考虑用联合类型书写 padLeft
代码。 可以像下面这样利用类型断言来写:
function isNumber(x: any): x is number { return typeof x === "number"; } function isString(x: any): x is string { return typeof x === "string"; } function padLeft(value: string, padding: string | number) { if (isNumber(padding)) { return Array(padding + 1).join(" ") + value; } if (isString(padding)) { return padding + value; } throw new Error(`Expected string or number, got '${padding}'.`); }
然而,每次typeof进行类型判断都必须要定义一个函数,这太痛苦了。 幸运的是,现在我们不必将 typeof x === "number"
抽象成一个函数,因为TypeScript可以将它识别为一个类型保护。 也就是说我们可以直接在代码里检查类型了。
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}'.`); }
typeof
类型保护只有两种形式能被识别: typeof v === "typename"
和 typeof v !== "typename"
,且typeof在TypeScript中使用时,只有匹配基本类型时(即"typename"
必须是 "number"
, "string"
, "boolean"
或 "symbol"
),才会启动类型保护 。 但是TypeScript并不会阻止你与其它字符串比较(例如typeof v === 'hello TypeScript'),typeof并不会把它识别为一个有效的类型,因此也不会把这些字符串识别为类型保护。
3.instanceof
类型保护
除了typeof以外,instanceof也可以起来类型保护的作用。Instanceof相较于typeof,其类型保护更为精细,是通过构造函数来区分类型的一种方式。
如果你已经阅读了 typeof
类型保护并且对JavaScript里的 instanceof
操作符熟悉的话,你可能已经很轻而易举的在TypeScript中使用instanceof
类型保护了。
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(" "); } // 类型为SpaceRepeatingPadder | StringPadder let padder: Padder = getRandomPadder(); if (padder instanceof SpaceRepeatingPadder) { padder; // 类型细化为'SpaceRepeatingPadder' } if (padder instanceof StringPadder) { padder; // 类型细化为'StringPadder' }
可以看出instanceof在类型的使用上,与typeof相比,可以将类作为比较对象,从而实现类型保护。
instanceof
的右侧要求是一个构造函数,TypeScript将细化为:
- 此构造函数的
prototype
属性的类型,如果它的类型不为any
的话 - 构造签名所返回的类型的联合
以此顺序。
⒋可以为null的类型
TypeScript具有两种特殊的类型, null
和 undefined
,它们分别具有值null和undefined. 我们在[基础类型](./Basic Types.md)一节里已经做过简要说明。 默认情况下,类型检查器认为 null
与 undefined
可以赋值给任何类型。 null
与 undefined
是所有其它类型的一个有效值。 这也意味着,你阻止不了将它们赋值给其它类型,就算是你想要阻止这种情况也不行。 null
的发明者,Tony Hoare,称它为 价值亿万美金的错误。
--strictNullChecks
标记可以解决此错误:当你声明一个变量时,它不会自动地包含 null
或 undefined
。 你可以使用联合类型明确的包含它们
let s = "foo"; s = null; // 错误, 'null'不能赋值给'string' let sn: string | null = "bar"; sn = null; // 可以 sn = undefined; // error, 'undefined'不能赋值给'string | null'
注意,按照JavaScript的语义,TypeScript会把 null
和 undefined
区别对待。 string | null
, string | undefined
和 string | undefined | 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; } }
这里很明显地去除了 null
,你也可以使用短路运算符:
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"); }
本例使用了嵌套函数,因为编译器无法去除嵌套函数的null(除非是立即调用的函数表达式)。 因为它无法跟踪所有对嵌套函数的调用,尤其是你将内层函数做为外层函数的返回值。 如果无法知道函数在哪里被调用,就无法知道调用时 name
的类型。
⒎类型别名
类型别名就是可以给一个类型起个新名字。类型别名有时和接口很像, 类型别名可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。
如果你学过C语言,可能还记得alias关键字,不过在TypeScript中,我们使用type关键字来描述类型变量。
type Name = string; type NameResolver = () => string; type NameOrResolver = Name | NameResolver; function getName(n: NameOrResolver): Name { if (typeof n === 'string') { return n; } else { return n(); } }
使用别名并不会在类型系统中新建一个类型 - 它创建了一个新的名字来引用那个类型。 给基本类型起别名通常没什么用,一般是用来减少文档的编写量。
类型别名也可以是泛型 - 我们可以添加类型参数并且在别名声明的右侧传入:
type Person<T> = {age : T};
也可以使用类型别名在属性里引用自己,这看起来很像是递归。
type Person<T> = { name : T; mother : Person<T>; father : Person<T>; }
这使得类型编排非常复杂。当然,这种复杂性是为了描述的准确性,正如上面的例子,mother和father肯定也是person。这样在代码中看上去有点不可思议的操作,在现实世界中却是非常真实合理的。
与交叉类型一起使用,我们可以创建出一些十分稀奇古怪的类型。
type LinkedList<T> = T & { next: LinkedList<T> }; interface Person { name: string; } 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
其它类型)。 因为 软件中的对象应该对于扩展是开放的,但是对于修改是封闭的,你应该尽量去使用接口代替类型别名。
另一方面,如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名。
⒏字符串字面量类型
我们先看一个简单的字面量类型,比如下面这个字符串常量。
type Profession = "teacher";
字符串字面量类型允许你指定字符串必须的固定值。
在实际应用中,通常字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合。 而通过结合使用这些特性,达到类似枚举类型的效果。
type Profession = "teacher" | "doctor" | "accountant"; function personCreator(Profession : Profession){ //省略函数内部的具体实现,这并不影响案例的运行 } personCreator("teacher"); personCreator("doctor"); personCreator("accountant"); personCreator("programmer"); //Error
你只能从三种允许的字符中选择其一来做为参数传递,传入其它值则会产生错误。【参见上面的联合类型】
字符串字面量类型还可以用于区分函数重载:
function createElement(tagName: "img"): HTMLImageElement; function createElement(tagName: "input"): HTMLInputElement; // ... more overloads ... function createElement(tagName: string): Element { // ... code goes here ... }
⒐数字字面量类型
TypeScript还具有数字字面量类型,其用法和字符串字面量一致。
function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 { // ... }
我们很少直接这样使用,但它们可以用在缩小范围调试bug的时候:
function foo(x: number) { if (x !== 1 || x !== 2) { // ~~~~~~~ // Operator '!==' cannot be applied to types '1' and '2'. } }
换句话说,当 x
与 2
进行比较的时候,它的值必须为 1
,这就意味着上面的比较检查是非法的。
⒑枚举成员类型
如我们在 枚举一节里提到的,当每个枚举成员都是用字面量初始化的时候枚举成员是具有类型的。
在我们谈及“单例类型”的时候,多数是指枚举成员类型和数字/字符串字面量类型,尽管大多数用户会互换使用“单例类型”和“字面量类型”。
可辨识联合(Discriminated Unions)
你可以合并单例类型,联合类型,类型保护和类型别名来创建一个叫做 可辨识联合的高级模式,它也称做 标签联合或 代数数据类型。 可辨识联合在函数式编程很有用处。 一些语言会自动地为你辨识联合;而TypeScript则基于已有的JavaScript模式。 它具有3个要素:
- 具有普通的单例类型属性— 可辨识的特征。
- 一个类型别名包含了那些类型的联合— 联合。
- 此属性上的类型保护。
interface Square { kind: "square"; size: number; } interface Rectangle { kind: "rectangle"; width: number; height: number; } interface Circle { kind: "circle"; radius: number; }
首先我们声明了将要联合的接口。 每个接口都有 kind
属性但有不同的字符串字面量类型。 kind
属性称做 可辨识的特征或 标签。 其它的属性则特定于各个接口。 注意,目前各个接口间是没有联系的。 下面我们把它们联合到一起:
type Shape = Square | Rectangle | Circle;
现在我们使用可辨识联合:
function area(s: Shape) { switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; } }
完整性检查
当没有涵盖所有可辨识联合的变化时,我们想让编译器可以通知我们。 比如,如果我们添加了 Triangle
到 Shape
,我们同时还需要更新 area
:
type Shape = Square | Rectangle | Circle | Triangle; function area(s: Shape) { switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; } // should error here - we didn't handle case "triangle" }
有两种方式可以实现。 首先是启用 --strictNullChecks
并且指定一个返回值类型:
function area(s: Shape): number { // error: returns number | undefined switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; } }
因为 switch
没有包涵所有情况,所以TypeScript认为这个函数有时候会返回 undefined
。 如果你明确地指定了返回值类型为 number
,那么你会看到一个错误,因为实际上返回值的类型为 number | undefined
。 然而,这种方法存在些微妙之处且 --strictNullChecks
对旧代码支持不好。
第二种方法使用 never
类型,编译器用它来进行完整性检查:
function assertNever(x: never): never { throw new Error("Unexpected object: " + x); } function area(s: Shape) { switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; default: return assertNever(s); // error here if there are missing cases } }
这里, assertNever
检查 s
是否为 never
类型—即为除去所有可能情况后剩下的类型。 如果你忘记了某个case,那么 s
将具有一个真实的类型并且你会得到一个错误。 这种方式需要你定义一个额外的函数,但是在你忘记某个case的时候也更加明显。
多态的 this
类型
多态的 this
类型表示的是某个包含类或接口的 子类型。 这被称做 F-bounded多态性。 它能很容易的表现连贯接口间的继承,比如。 在计算器的例子里,在每个操作之后都返回 this
类型:
class BasicCalculator { public constructor(protected value: number = 0) { } public currentValue(): number { return this.value; } public add(operand: number): this { this.value += operand; return this; } public multiply(operand: number): this { this.value *= operand; return this; } // ... other operations go here ... } let v = new BasicCalculator(2) .multiply(5) .add(1) .currentValue();
由于这个类使用了 this
类型,你可以继承它,新的类可以直接使用之前的方法,不需要做任何的改变。
class ScientificCalculator extends BasicCalculator { public constructor(value = 0) { super(value); } public sin() { this.value = Math.sin(this.value); return this; } // ... other operations go here ... } let v = new ScientificCalculator(2) .multiply(5) .sin() .add(1) .currentValue();
如果没有 this
类型, ScientificCalculator
就不能够在继承 BasicCalculator
的同时还保持接口的连贯性。 multiply
将会返回 BasicCalculator
,它并没有 sin
方法。 然而,使用 this
类型, multiply
会返回 this
,在这里就是 ScientificCalculator
。
索引类型(Index types)
使用索引类型,编译器就能够检查使用了动态属性名的代码。例如,一个常见的JavaScript模式是从对象中选取属性的子集。
function pluck(o, names) { return names.map(n => o[n]); }
在TypeScript里通过 索引类型查询和 索引访问操作符使用此函数:
function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] { return names.map(n => o[n]); } interface Person { name: string; age: number; } let person: Person = { name: 'Jarid', age: 35 }; let strings: string[] = pluck(person, ['name']); // ok, string[]
编译器会检查传入的值是否是 Person
的一个属性。 本例还引入了几个新的类型操作符。 首先是 keyof T
, 索引类型查询操作符。 对于任何类型 T
, keyof T
的结果为 T
上已知的公共属性名的联合。 例如:
let personProps: keyof Person; // 'name' | 'age'
keyof Person
是完全可以与 'name' | 'age'
互相替换的。 不同的是如果你为Person添加了新的属性 ,例如 address: string
,那么 keyof Person
会自动变为 'name' | 'age' | 'address'
。 你可以在像 pluck
函数这类上下文里使用 keyof
,因为在使用之前你并不清楚可能出现的属性名。 但编译器会检查你是否传入了正确的属性名给 pluck
:
pluck(person, ['age', 'unknown']); // error, 'unknown' is not in 'name' | 'age'
第二个操作符是 T[K]
, 索引访问操作符。 在这里,类型语法反映了表达式语法。 这意味着 person['name']
具有类型 Person['name']
— 在我们的例子里则为 string
类型。 然而,就像索引类型查询一样,你可以在普通的上下文里使用 T[K]
,这正是它的强大所在。 你只要确保类型变量 K extends keyof T
就可以了。 例如下面 getProperty
函数的例子:
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] { return o[name]; // o[name] is of type T[K] }
getProperty
里的 o: T
和 name: K
,意味着 o[name]: T[K]
。 当你返回 T[K]
的结果,编译器会实例化键的真实类型,因此 getProperty
的返回值类型会随着你需要的属性改变。
let name: string = getProperty(person, 'name'); let age: number = getProperty(person, 'age'); let unknown = getProperty(person, 'unknown'); // error, 'unknown' is not in 'name' | 'age'
索引类型和字符串索引签名
keyof
和 T[K]
与字符串索引签名进行交互。 如果你有一个带有字符串索引签名的类型,那么 keyof T
会是 string
。 并且 T[string]
为索引签名的类型:
interface Map<T> { [key: string]: T; } let keys: keyof Map<number>; // string let value: Map<number>['foo']; // number
让我们解释下上面一开始代码的意义。首先看泛型,这里有T和K两种类型。根据类型推断,第一个参数o就是person,类型会被推断为Person,而第二个数组参数的类型推断,我们可以从右往左进行阅读,keyof关键字可以获取T(此处为Person)的所有属性名,即['name','age'],泛型K通过extends关键字继承了T(此处为Person)的所有属性名,即['name','age']。
依托于keyof关键字完成了类型索引。
我们再来看返回值,返回值的类型是T[K][],阅读起来有些困难,它实际上表述的意思是,变量T取属性K的值的数组,其中T[K]就是索引访问操作符。
这样强大的功能保证了代码的动态性和准确性,也让代码提示变得更加丰富了。
映射类型
一种常见的场景是将一个已知类型的每个属性都变为可选的,这样在实例化该类型时就不必为每个类型都赋值了。
interface Person { name?: string; age?: number; }
或者是我们想要一个该类型的只读版本【即该类型的属性值都是只读不可修改的】
interface Person {
readonly name: string;
readonly age: number;
}
这在JavaScript里会经常用到,而TypeScript提供了从旧类型中创建新类型的一种方式 — 映射类型。 在映射类型里,新类型以相同的形式去转换旧类型里每个属性。 例如,你可以令每个属性成为只读类型或可选类型。 下面是一些例子:
type Readonly<T> = { readonly [P in keyof T]: T[P]; } type Partial<T> = { [P in keyof T]?: T[P]; }
像下面这样使用:
type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;
下面来看看最简单的映射类型和它的组成部分:
type Keys = 'option1' | 'option2'; type Flags = { [K in Keys]: boolean };
它的语法与索引签名的语法类型,内部使用了 for .. in
。 具有三个部分:
- 类型变量
K
,它会依次绑定到每个属性。 - 字符串字面量联合的
Keys
,它包含了要迭代的属性名的集合。 - 属性的结果类型。
在个简单的例子里, Keys
是硬编码的的属性名列表并且属性类型永远是 boolean
,因此这个映射类型等同于:
type Flags = { option1: boolean; option2: boolean; }
在真正的应用里,可能不同于上面的 Readonly
或 Partial
。 它们会基于一些已存在的类型,且按照一定的方式转换字段。 这就是 keyof
和索引访问类型要做的事情:
type NullablePerson = { [P in keyof Person]: Person[P] | null } type PartialPerson = { [P in keyof Person]?: Person[P] }
但它更有用的地方是可以有一些通用版本。
type Nullable<T> = { [P in keyof T]: T[P] | null } type Partial<T> = { [P in keyof T]?: T[P] }
在这些例子里,属性列表是 keyof T
且结果类型是 T[P]
的变体。 这是使用通用映射类型的一个好模版。 因为这类转换是 同态的,映射只作用于 T
的属性而没有其它的。 编译器知道在添加任何新属性之前可以拷贝所有存在的属性修饰符。 例如,假设 Person.name
是只读的,那么 Partial<Person>.name
也将是只读的且为可选的。
下面是另一个例子, T[P]
被包装在 Proxy<T>
类里:
type Proxy<T> = { get(): T; set(value: T): void; } type Proxify<T> = { [P in keyof T]: Proxy<T[P]>; } function proxify<T>(o: T): Proxify<T> { // ... wrap proxies ... } let proxyProps = proxify(props);
注意 Readonly<T>
和 Partial<T>
用处不小,因此它们与 Pick
和 Record
一同被包含进了TypeScript的标准库里:
type Pick<T, K extends keyof T> = { [P in K]: T[P]; } type Record<K extends string, T> = { [P in K]: T; }
Readonly
, Partial
和 Pick
是同态的,但 Record
不是。 因为 Record
并不需要输入类型来拷贝属性,所以它不属于同态:
type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>
非同态类型本质上会创建新的属性,因此它们不会从它处拷贝属性修饰符。
TypeScript中内置了Readonly和Partial,所以不需要手动声明实现。
内置的类型还有Required、Pick、Record、Exclude、Extract、NonNullable;它们的实现都在typescript/lib/lib.es5.d.ts中。
由映射类型进行推断
现在你了解了如何包装一个类型的属性,那么接下来就是如何拆包。 其实这也非常容易:
function unproxify<T>(t: Proxify<T>): T { let result = {} as T; for (const k in t) { result[k] = t[k].get(); } return result; } let originalProps = unproxify(proxyProps);
注意这个拆包推断只适用于同态的映射类型。 如果映射类型不是同态的,那么需要给拆包函数一个明确的类型参数。
预定义的有条件类型
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>
-- 获取构造函数类型的实例类型。
示例
type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "b" | "d" type T01 = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "a" | "c" type T02 = Exclude<string | number | (() => void), Function>; // string | number type T03 = Extract<string | number | (() => void), Function>; // () => void type T04 = NonNullable<string | number | undefined>; // string | number type T05 = NonNullable<(() => string) | string[] | null | undefined>; // (() => string) | string[] function f1(s: string) { return { a: 1, b: s }; } class C { x = 0; y = 0; } type T10 = ReturnType<() => string>; // string type T11 = ReturnType<(s: string) => void>; // void type T12 = ReturnType<(<T>() => T)>; // {} type T13 = ReturnType<(<T extends U, U extends number[]>() => T)>; // number[] type T14 = ReturnType<typeof f1>; // { a: number, b: string } type T15 = ReturnType<any>; // any type T16 = ReturnType<never>; // any type T17 = ReturnType<string>; // Error type T18 = ReturnType<Function>; // Error type T20 = InstanceType<typeof C>; // C type T21 = InstanceType<any>; // any type T22 = InstanceType<never>; // any type T23 = InstanceType<string>; // Error type T24 = InstanceType<Function>; // Error
注意:Exclude
类型是建议的Diff
类型的一种实现。我们使用Exclude
这个名字是为了避免破坏已经定义了Diff
的代码,并且我们感觉这个名字能更好地表达类型的语义。我们没有增加Omit<T, K>
类型,因为它可以很容易的用Pick<T, Exclude<keyof T, K>>
来表示。