在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
⒈交叉类型(Intersection Types) 交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。 例如, 每当我们正确的使用交叉类型的时候,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" let indentedString = padLeft("Hello world", true); // 编译阶段通过,运行时报错 在传统的面向对象语言里,我们可以使用重载或将这两种类型抽象成有层级的类型(父类与子类)。 这么做显然是非常清晰的,但同时也存在了过度设计。 因为在JavaScript中并没有重载可以使用(可以使用特殊的方式创建出类似重载的函数),因此在JavaScript的函数中手动去判断参数的类型这种操作更为常见,这在一定程度上避免了过度设计。 如果我们希望更准确的描述padding的类型,就可以使用联合类型将padding的类型限定为既可以是number又可以是string。 代替 function padLeft(value: string, padding: string | number) { // ... } let indentedString = padLeft("Hello world", true); // 编译器报错,类型true的参数不能赋值给类型string|number的参数 联合类型表示一个变量可以是几种类型之一。 我们用竖线( 注意,如果一个值是联合类型,我们只能访问它们共有的属性或方法。 我们来看一下下面的例子: interface Bird { fly(); layEggs(); } interface Fish { swim(); layEggs(); } function getSmallPet(): Fish | Bird { // ... } let pet = getSmallPet(); pet.layEggs(); // okay pet.swim(); // errors 如果一个值的类型是 联合类型取的是交集,交叉类型取的是并集,这听起来和它们的名字有些冲突。 **谨记,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; } 在这个例子里, 每当使用一些变量调用 // 'swim' 和 'fly' 调用都没有问题了 if (isFish(pet)) { pet.swim(); } else { pet.fly(); } 注意,TypeScript不仅知道在
2. 现在我们可以使用类型保护来重构一开始的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进行类型判断都必须要定义一个函数,这太痛苦了。 幸运的是,现在我们不必将 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}'.`); }
3. 除了typeof以外,instanceof也可以起来类型保护的作用。Instanceof相较于typeof,其类型保护更为精细,是通过构造函数来区分类型的一种方式。 如果你已经阅读了
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相比,可以将类作为比较对象,从而实现类型保护。
以此顺序。 ⒋可以为null的类型 TypeScript具有两种特殊的类型,
let s = "foo"; s = null; // 错误, 'null'不能赋值给'string' let sn: string | null = "bar"; sn = null; // 可以 sn = undefined; // error, 'undefined'不能赋值给'string | null' 注意,按照JavaScript的语义,TypeScript会把
⒌可选参数和可选属性 使用了 -- 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的类型是通过联合类型实现,那么你需要使用类型保护来去除 function f(sn: string | null): string { if (sn == null) { return "default"; } else { return sn; } } 这里很明显地去除了 function f(sn: string | null): string { return sn || "default"; } 如果编译器不能够去除 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(除非是立即调用的函数表达式)。 因为它无法跟踪所有对嵌套函数的调用,尤其是你将内层函数做为外层函数的返回值。 如果无法知道函数在哪里被调用,就无法知道调用时
⒎类型别名 类型别名就是可以给一个类型起个新名字。类型别名有时和接口很像, 类型别名可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。 如果你学过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. 类型别名像我们提到的,类型别名可以像接口一样;然而,仍有一些细微差别。 其一,接口创建了一个新的名字,可以在其它任何地方使用。 类型别名并不创建新名字—比如,错误信息就不会使用别名。 在下面的示例代码里,在编译器中将鼠标悬停在 type Alias = { num: number } interface Interface { num: number; } declare function aliased(arg: Alias): Alias; declare function interfaced(arg: Interface): Interface; 另一个重要区别是类型别名不能被 另一方面,如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名。
⒏字符串字面量类型 我们先看一个简单的字面量类型,比如下面这个字符串常量。 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'. } } 换句话说,当
⒑枚举成员类型 如我们在 枚举一节里提到的,当每个枚举成员都是用字面量初始化的时候枚举成员是具有类型的。 在我们谈及“单例类型”的时候,多数是指枚举成员类型和数字/字符串字面量类型,尽管大多数用户会互换使用“单例类型”和“字面量类型”。 你可以合并单例类型,联合类型,类型保护和类型别名来创建一个叫做 可辨识联合的高级模式,它也称做 标签联合或 代数数据类型。 可辨识联合在函数式编程很有用处。 一些语言会自动地为你辨识联合;而TypeScript则基于已有的JavaScript模式。 它具有3个要素:
interface Square { kind: "square"; size: number; } interface Rectangle { kind: "rectangle"; width: number; height: number; } interface Circle { kind: "circle"; radius: number; } 首先我们声明了将要联合的接口。 每个接口都有 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; } } 完整性检查 当没有涵盖所有可辨识联合的变化时,我们想让编译器可以通知我们。 比如,如果我们添加了 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" } 有两种方式可以实现。 首先是启用 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; } } 因为 第二种方法使用 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 } } 这里, 多态的
|
请发表评论