2019-11-09:
学习内容:类型推论、类型兼容性、高级类型
一、类型推论:类型是在哪里如何被推断的
在有些没有明确指出类型的地方,类型推论会帮助提供类型。如:let x = 3; 变量x
的类型被推断为数字。 这种推断发生在初始化变量和成员,设置默认参数值和决定函数返回值时。大多数情况下,类型推论是直截了当地。
最佳通用类型:如: let x = [0, 1, null]; 两种选择:number 和 null , 计算通用类型算法会考虑所有的候选类型,并给出一个兼容所有候选类型的类型。如果没有找到最佳通用类型的话,类型推断的结果为联合数组类型。
上下文类型:
TypeScript类型推论也可能按照相反的方向进行。 这被叫做“按上下文归类”。按上下文归类会发生在表达式的类型与所处的位置相关时。
window.onmousedown = function(mouseEvent) { console.log(mouseEvent.button); //<- Error };
这个例子会得到一个类型错误,TypeScript类型检查器使用Window.onmousedown
函数的类型来推断右边函数表达式的类型。 因此,就能推断出 mouseEvent
参数的类型了。 如果函数表达式不是在上下文类型的位置, mouseEvent
参数的类型需要指定为any
,这样也不会报错了。
二、类型的兼容性:
TypeScript里的类型兼容性是基于结构子类型的。 结构类型是一种只使用其成员来描述类型的方式。 它正好与名义(nominal)类型形成对比。(译者注:在基于名义类型的类型系统中,数据类型的兼容性或等价性是通过明确的声明和/或类型的名称来决定的。这与结构性类型系统不同,它是基于类型的组成结构,且不要求明确地声明。)
TypeScript的类型系统允许某些在编译阶段无法确认其安全性的操作。当一个类型系统具此属性时,被当做是“不可靠”的。TypeScript允许这种不可靠行为的发生是经过仔细考虑的。通过这篇文章,我们会解释什么时候会发生这种情况和其有利的一面。
TypeScript结构化类型系统的基本规则是,如果x
要兼容y
,那么y
至少具有与x
相同的属性。
interface Named { name: string; } let x: Named; // y's inferred type is { name: string; location: string; } let y = { name: 'Alice', location: 'Seattle' }; x = y;
这里要检查y
是否能赋值给x
,编译器检查x
中的每个属性,看是否能在y
中也找到对应属性。 在这个例子中,y
必须包含名字是name
的string
类型成员。y
满足条件,因此赋值正确。
(2)比较两个函数:
如果数量及相对应类型有不同,都不能赋值。
(3)可选参数及剩余参数:
比较函数兼容性的时候,可选参数与必须参数是可互换的。 源类型上有额外的可选参数不是错误,目标类型的可选参数在源类型里没有对应的参数也不是错误。
当一个函数有剩余参数时,它被当做无限个可选参数。
这对于类型系统来说是不稳定的,但从运行时的角度来看,可选参数一般来说是不强制的,因为对于大多数函数来说相当于传递了一些undefinded
(4)函数重载:
对于有重载的函数,源函数的每个重载都要在目标函数上找到对应的函数签名。 这确保了目标函数可以在所有源函数可调用的地方调用。
(5)枚举:
枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的。
(6)类:
类与对象字面量和接口差不多,但有一点不同:类有静态部分和实例部分的类型。 比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不在比较的范围内。
class Animal { feet: number; constructor(name: string, numFeet: number) { } } class Size { feet: number; constructor(numFeet: number) { } } let a: Animal; let s: Size; a = s; // OK s = a; // OK
类的私有成员(private)和受保护(protected)成员会影响兼容性。 当检查类实例的兼容时,如果目标类型包含一个私有成员,那么源类型必须包含来自同一个类的这个私有成员。 同样地,这条规则也适用于包含受保护成员实例的类型检查。 这允许子类赋值给父类,但是不能赋值给其它有同样类型的类。
(7)注意:
目前为止,我们使用了“兼容性”,它在语言规范里没有定义。 在TypeScript里,有两种兼容性:子类型和赋值。 它们的不同点在于,赋值扩展了子类型兼容性,增加了一些规则,允许和any
来回赋值,以及enum
和对应数字值之间的来回赋值。
语言里的不同地方分别使用了它们之中的机制。 实际上,类型兼容性是由赋值兼容性来控制的,即使在implements
和extends
语句也不例外。
三、高级类型:
(1)交叉类型:
将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。 例如, Person & Serializable & Loggable
同时是 Person
和 Serializable
和 Loggable
。 就是说这个类型的对象同时拥有了这三种类型的成员。
交叉类型的含义为:符合类型 A 和 B 的交叉类型的值,既符合类型 A,又符合类型 B。
类比前文的联合类型,交叉类型可以认为是两个类型的交集。其内涵覆盖了原来两个集合的所有内涵。
(2)自定义的类型保护:
TypeScript里的 类型保护机制让它成为了现实。 类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。 要定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个 类型谓词:
function isFish(pet: Fish | Bird): pet is Fish { return (<Fish>pet).swim !== undefined; }
pet is Fish
就是类型谓词。 谓词为 parameterName is Type
这种形式, parameterName
必须是来自于当前函数签名里的一个参数名。
每当使用一些变量调用 isFish
时,TypeScript会将变量缩减为那个具体的类型,只要这个类型与变量的原始类型是兼容的。
(3)typeof的类型保护:
这些* typeof
类型保护*只有两种形式能被识别: typeof v === "typename"
和 typeof v !== "typename"
, "typename"
必须是 "number"
, "string"
, "boolean"
或 "symbol"
。 但是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}'.`); }
(4)instanceof 类型保护:
instanceof
类型保护是通过构造函数来细化类型的一种方式。
instanceof
的右侧要求是一个构造函数,TypeScript将细化为:
- 此构造函数的
prototype
属性的类型,如果它的类型不为any
的话 - 构造签名所返回的类型的联合
以此顺序。
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' }
(5)类型别名:(尽量少用,多用接口)
类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。
type Name = string; type NameResolver = () => string; type NameOrResolver = Name | NameResolver; function getName(n: NameOrResolver): Name { if (typeof n === 'string') { return n; } else { return n(); } }
类型别名可以像接口一样;然而,仍有一些细微差别。
其一,接口创建了一个新的名字,可以在其它任何地方使用。 类型别名并不创建新名字—比如,错误信息就不会使用别名。
另一个重要区别是类型别名不能被 extends
和 implements
(自己也不能 extends
和 implements
其它类型)。 因为 软件中的对象应该对于扩展是开放的,但是对于修改是封闭的,你应该尽量去使用接口代替类型别名
(6)字符串(/数字)字面量类型:
允许你指定字符串必须的固定值。 在实际应用中,字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合。 通过结合使用这些特性,你可以实现类似枚举类型的字符串。
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
(7)可辨识联合:
你可以合并单例类型,联合类型,类型保护和类型别名来创建一个叫做 可辨识联合的高级模式,它也称做 标签联合或 代数数据类型。 可辨识联合在函数式编程很有用处。 一些语言会自动地为你辨识联合;而TypeScript则基于已有的JavaScript模式。 它具有3个要素:
- 具有普通的单例类型属性— 可辨识的特征。
- 一个类型别名包含了那些类型的联合— 联合。
- 此属性上的类型保护。
interface Square { kind: "square"; size: number; } interface Rectangle { kind: "rectangle"; number; height: number; } interface Circle { kind: "circle"; radius: number; }
每个接口都有 kind
属性但有不同的字符串字面量类型。 kind
属性称做 可辨识的特征或 标签。 其它的属性则特定于各个接口。 注意,目前各个接口间是没有联系的。
重复属性的联合:
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; } }
(8)多态的this 类型:
多态的 this
类型表示的是某个包含类或接口的 子类型。 这被称做 F-bounded多态性。 它能很容易的表现连贯接口间的继承,
由于这个类使用了 this
类型,你可以继承它,新的类可以直接使用之前的方法,不需要做任何的改变。
(9)索引类型:
使用索引类型,编译器就能够检查使用了动态属性名的代码。
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[]
编译器会检查 name
是否真的是 Person
的一个属性。 本例还引入了几个新的类型操作符。 首先是 keyof T
, 索引类型查询操作符。 对于任何类型 T
, keyof T
的结果为 T
上已知的公共属性名的联合。正如:
keyof Person
是完全可以与 'name' | 'age'
互相替换的。 不同的是如果你添加了其它的属性到 Person
,例如 address: string
,那么 keyof Person
会自动变为 'name' | 'age' | 'address'
。
T[K]
, 索引访问操作符。 在这里,类型语法反映了表达式语法。 这意味着 person['name']
具有类型 Person['name']
— 在我们的例子里则为 string
类型。 然而,就像索引类型查询一样,你可以在普通的上下文里使用 T[K]
,这正是它的强大所在。 你只要确保类型变量 K extends keyof T
就可以了。
(10)映射类型:
TypeScript提供了从旧类型中创建新类型的一种方式 — 映射类型。 在映射类型里,新类型以相同的形式去转换旧类型里每个属性。 例如,你可以令每个属性成为 readonly
类型或可选的。
(后续补充。。。)