一、交叉类型
交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。
二、联合类型
联合类型表示一个值可以是几种类型之一。 我们用竖线( |)分隔每个类型,所以 number | string | boolean表示一个值可以是 number, string,或 boolean。
/**
* 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);
如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。
interface Bird {
fly();
layEggs();
}
interface Fish {
swim();
layEggs();
}
function getSmallPet(): Fish | Bird {
// ...
}
let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim(); // 类型“Bird | Fish”上不存在属性“swim”。类型“Bird”上不存在属性“swim”。
三、类型保护与区分类型
- 检查成员是否存在,并且只能访问联合类型中共同拥有的成员。
let pet = getSmallPet();
// 每一个成员访问都会报错
if (pet.swim) {
pet.swim();
}
else if (pet.fly) {
pet.fly();
}
- 类型断言。
let pet = getSmallPet();
if ((<Fish>pet).swim) {
(<Fish>pet).swim();
}
else {
(<Bird>pet).fly();
}
四、用户自定义的类型保护
类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。 要定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个 类型谓词:
function isFish(pet: Fish | Bird): pet is Fish { // "parameterName is Type": parameterName必须是来自于当前函数签名里的一个参数名。
return (<Fish>pet).swim !== undefined;
}
五、typeof类型保护
我们不必将 typeof x === "number"抽象成一个函数,因为TypeScript可以将它识别为一个类型保护。
// 使用联合类型
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
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 v === "typename"和 typeof v !== "typename", "typename"必须是 "number", "string", "boolean"或 "symbol"。
六、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的右侧要求是一个构造函数,TypeScript将细化为:
此构造函数的 prototype属性的类型,如果它的类型不为 any的话
构造签名所返回的类型的联合
七、可以为null的类型
let s = "foo"; // 类型推论为number
s = null; // 错误, 'null'不能赋值给'string'
let sn: string | null = "bar";
sn = null; // 可以
sn = undefined; // error, 'undefined'不能赋值给'string | null'
TypeScript会把 null和 undefined区别对待。 string | null, string | undefined和 string | undefined | null是不同的类型。
八、可选参数和可选属性
// 可选参数
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'
九、类型保护和类型断言
function f(sn: string | null): string {
if (sn == null) {
return "default";
}
else {
return sn;
}
// 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");
}
// 等同于
function getName1(n: string | (() => string)): Name {
if (typeof n === 'string') {
return n;
}
else {
return n();
}
}
var res = getName(() => '我是天才')
编译器无法去除嵌套函数的null(除非是立即调用的函数表达式)。
十、类型别名
类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
if (typeof n === 'string') {
return n;
}
else {
return n();
}
}
接口 vs 类型别名 - 接口创建了一个新的名字,可以在其它任何地方使用。 - 类型别名并不创建新名字—比如,错误信息就不会使用别名。 - 类型别名不能被 extends和 implements(自己也不能 extends和 implements其它类型)。 - 无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名。
十一、字符串字面量类型
字符串字面量类型允许你指定字符串必须的固定值。 在实际应用中,字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合。 通过结合使用这些特性,你可以实现类似枚举类型的字符串。
type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
animate(dx: number, dy: number, easing: Easing) {
if (easing === "ease-in") {
// ...
}
else if (easing === "ease-out") {
}
else if (easing === "ease-in-out") {
}
else {
// error! should not pass null or undefined.
}
}
}
let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); // error: "uneasy" is not allowed here, Argument of type '"uneasy"' is not assignable to parameter of type '"ease-in" | "ease-out" | "ease-in-out"'
十二、数字字面量类型
function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 {
// ...
}
十三、枚举成员类型
十四、可辨识联合
你可以合并单例类型,联合类型,类型保护和类型别名来创建一个叫做 可辨识联合的高级模式,它也称做 标签联合或 代数数据类型。 可辨识联合在函数式编程很有用处。 一些语言会自动地为你辨识联合;而TypeScript则基于已有的JavaScript模式。 它具有3个要素:
- 具有普通的单例类型属性— 可辨识的特征。
- 一个类型别名包含了那些类型的联合— 联合。
- 此属性上的类型保护。
interface Square {
kind: "square"; // kind属性称做 可辨识的特征 或 标签。
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
完整性检查
- 首先是启用 --strictNullChecks并且指定一个返回值类型:
type Shape = Square | Rectangle | Circle; // 定义 可辨识联合
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;
}
}
- 使用 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
}
}
十五、多态的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();
// 科学计算器
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();
十六、索引类型
使用索引类型,编译器就能够检查使用了动态属性名的代码。
通过 索引类型查询和 索引访问操作符:
编译器会检查 name是否真的是 Person的一个属性。
// keyof T, 索引类型查询操作符。对于任何类型 T, keyof T的结果为 T上已知的公共属性名的联合。
function pluck<T, K extends keyof T>(o: T, names: K[]): T[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[]
pluck(person, ['age', 'unknown']); // error, 'unknown' is not in 'name' | 'age'
然而,就像索引类型查询一样,你可以在普通的上下文里使用 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]
}
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
十七、映射类型
TypeScript提供了从旧类型中创建新类型的一种方式 — 映射类型。 在映射类型里,新类型以相同的形式去转换旧类型里每个属性。
interface PersonPartial {
name?: string;
age?: number;
}
interface PersonReadonly {
readonly name: string;
readonly age: number;
}
type Readonly<T> = {
readonly [P in keyof T]: T[P];
}
type Partial<T> = {
[P in keyof T]?: T[P];
}
// 可以令每个属性成为 readonly类型或可选的。
type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;
最简单的映射类型和它的组成部分:
type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };
它的语法与索引签名的语法类型,内部使用了 for .. in。
- 类型变量 K,它会依次绑定到每个属性。
- 字符串字面量联合的 Keys,它包含了要迭代的属性名的集合。
- 属性的结果类型。
包装:
interface Person {
name: string;
}
type Proxy<T> = {
get(): T;
set(value: T): void;
}
type Person1 = Proxy<Person>
type Proxify<T> = {
[P in keyof T]: Proxy<T[P]>;
}
type Person2 = Proxify<Person>
function proxify<T>(o: T): Person2 {
// ... wrap proxies ...
return {
name: {
get() { // es6 访问器
return 'hello'
},
set() {
}
}
}
}
let props: Person = {
name: 'haha'
}
let proxyProps = proxify<Person>(props);
console.log(proxyProps);
拆包:
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); // name get, age get...
十八、预定义的有条件类型
Exclude<T, U> -- 从T中剔除可以赋值给U的类型。
Extract<T, U> -- 提取T中可以赋值给U的类型。
NonNullable -- 从T中剔除null和undefined。
ReturnType -- 获取函数返回值类型。
InstanceType -- 获取构造函数类型的实例类型。