本文属转载, 文脚注有来源
2.1 "鸭子" 类型
"鸭子" 类型??(黑人问号), 第一次看到这名词我也很懵逼, 其实它说的是结构型类型, 而目前类型检测主要分为结构型 (structural) 类型以及名义型 (nominal) 类型.
- interface Point2D {
- x: number;
- y: number;
- }
- interface Point3D {
- x: number;
- y: number;
- z: number;
- }
- var point2D: Point2D = { x:0, y: 10}
- var point3D: Point3D = { x: 0, y: 10, z: 20}
- function iTakePoint2D(point: Point2D) { /*do sth*/ }
- iTakePoint2D(point2D); // 类型匹配
- iTakePoint2D(point3D); // 类型兼容, 结构类型
- iTakePoint2D({ x:0 }); // 错误: missing information `y`
区别
结构型类型中的类型检测和判断的依据是类型的结构, 会看它有哪些属性, 分别是什么类型; 而不是类型的名称或者类型的 id.
名义类型是静态语言 Java,C 等语言所使用的, 简单来说就是, 如果两个类型的类型名不同, 那么这两个类型就是不同的类型了, 尽管两个类型是相同的结构.
Typescript 中的类型是结构型类型, 类型检查关注的是值的形状, 即鸭子类型 duck typing, 而且一般通过 interface 定义类型, 其实就是定义形状与约束~ 所以定义 interface 其实是针对结构来定义新类型. 对于 Typescript 来说, 两个类型只要结构相同, 那么它们就是同样的类型.
2.2 类型判断 / 区分类型
知道了 typescript 是个'鸭子类型'后, 我们就会想到一个问题, ts 这种鸭子类型怎么判断类型啊, 比如下面这个例子:
- public convertString2Image(customizeData: UserDataType) {
- if (Helper.isUserData(customizeData)) {
- const errorIcon = searchImageByName(this.iconImage, statusIconKey);
- if (errorIcon) {
- (customizeData as UserData).title.icon = errorIcon;
- }
- } else if (Helper.isUserFloorData(customizeData)) {
- // do nothing
- } else {
- // UserAlertData
- let targetImg;
- const titleIcon = (customizeData as UserAlertData)!.title.icon;
- if (targetImg) {
- (customizeData as UserAlertData).title.icon = targetImg;
- }
- }
- return customizeData;
- }
该方法是根据传入的用户数据来将传入的 icon 字段用实际对应的图片填充, customizeData 是用户数据, 此时我们需要根据不同类型来调用 searchImageByName 方法去加载对应的图片, 所以我们此时需要通过一些类型判断的方法在运行时判断出该对象的类型.
基础的类型判断
基本的类型判断方法我们可能会想到 typeof 和 instanceof, 在 ts 中, 其实也可以使用这两个操作符来判断类型, 比如:
使用 typeof 判断类型
- function doSomething(x: number | string) {
- if(typeof x === 'string') {
- console.log(x.toFixed()); // Property 'toFixed' does not exist on type 'string'
- console.log(x.substr(1));
- } else if (typeof x === 'number') {
- console.log(x.toFixed());
- console.log(x.substr(1)); // Property 'substr' does not exist on type 'number'.
- }
- }
可以看到使用 typeof 在运行时判断基础数据类型是可行的, 可以在不同的条件块中针对不同的类型执行不同的业务逻辑, 但是对于 Class 或者 Interface 定义的非基础类型, 就必须考虑其他方式了.
使用 instanceof 判断类型 下面这个例子根据传入的 geo 对象的类型执行不同的处理逻辑:
- public addTo(geo: IMap | IArea | Marker) {
- this.gisObj = geo;
- this.container = this.draw()!;
- if (!this.container) {
- return;
- }
- this.mapContainer.appendChild<htmlDivElement>(this.container!);
- if (this.gisObj instanceof IMap) {
- this.handleDuration();
- } else if(this.gisObj instanceof Marker) {
- //
- }
- }
可以看到, 使用 instanceof 动态地判断类型是可行的, 而且类型可以是 Class 关键字声明的类型, 这些类型都拥有复杂的结构, 而且拥有构造函数. 总地来说, 使用 instanceof 判断类型的两个条件是:
必须是拥有构造函数的类型, 比如类类型.
构造函数 prototype 属性类型不能为 any.
利用类型谓词来判断类型 结合一开始的例子, 我们要去判断一个鸭子类型, 在 ts 中, 我们有特殊的方式, 就是类型谓词 (type predicate) 的概念, 这是 typescript 的类型保护机制, 它会在运行时检查确保在特定作用域内的类型. 针对那些 Interface 定义的类型以及映射出来的类型, 而且它并不具有构造函数, 所以我们需要自己去定义该类型的检查方法, 通常也被称为类型保护.
例子中的调用的两个基于类型保护的方法的实现
- public static isUserData(userData: UserDataType): userData is UserData {
- return ((userData as UserData).title !== undefined) && ((userData as UserData).subTitle !== undefined)
- && ((userData as UserData).body !== undefined) && ((userData as UserData).type === USER_DATA_TYPE.USER_DATA);
- }
- public static isUserFloorData(userFloorData: UserDataType): userFloorData is UserFloorData {
- return ((userFloorData as UserFloorData).deviceAllNum !== undefined)
- && ((userFloorData as UserFloorData).deviceNormalNum !== undefined)
- && ((userFloorData as UserFloorData).deviceFaultNum !== undefined)
- && ((userFloorData as UserFloorData).deviceOfflineNum !== undefined);
- }
实际上, 我们要去判断这个类型的结构, 这也是为什么 ts 的类型系统被称为鸭子类型, 我们需要遍历对象的每一个属性来区分类型. 换句话说, 如果定义了两个结构完全相同的类型, 即便类型名不同也会判断为相同的类型~
2.3 索引类型干嘛用?
索引类型(index types), 使用索引类型, 编译器就能够检查使用了动态属性名的代码. ts 中通过索引访问操作符 keyof 获取类型中的属性名, 比如下面的例子:
- 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']);
原理 编译器会检查 name 是否真的为 person 的一个属性, 然后 keyof T, 索引类型查询操作符, 对于任何类型 T, keyof T 的结果为 T 上已知的属性名的联合.
let personProps: keyof Person; // 'name' | 'age'
也就是说, 属性名也可以是任意的 interface 类型!
索引访问操作符 T[K]
索引类型指的其实 ts 中的属性可以是动态类型, 在运行时求值时才知道类型. 你可以在普通的上下文中使用 T[K]类型, 只需要确保 K extends keyof T 即可, 例如下面:
- function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
- return o[name];
- }
原理: o:T 和 name:K 表示 o[name]: T[K] 当你返回 T[K] 的结果, 编译器会实例化 key 的真实类型, 因此 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] 与字符串索引签名进行交互. 比如:
- interface Map<T> {
- [key: string]: T; // 这是一个带有字符串索引签名的类型, keyof T 是 string
- }
- let keys: keyof Map<number>; // string
- let value: Map<number>['foo']; // number
Map<T > 是一个带有字符串索引签名的类型, 那么 keyof T 会是 string.
2.4 映射类型
背景 在使用 typescript 时, 会有一个问题我们是绕不开的 --> 如何从旧的类型中创建新类型即映射类型.
- interface PersonPartial {
- name?: string;
- age?: number;
- }
- interface PersonReadonly {
- readonly name: string;
- readonly age: number;
- }
可以看到 PersonReadOnly 这个类型仅仅是对 PersonParial 类型的字段只读化设置, 想象一下 如果这个类型是 10 个字段那就需要重复写这 10 个字段. 我们有没办法不去重复写这种样板代码, 而是通过映射得到新类型? 答案就是映射类型,
映射类型的原理 新类型以相同的形式去转换旧类型里每个属性:
- type Readonly<T> {
- readonly [P in keyof T]: T[P];
- }
它的语法类似于索引签名的语法, 有三个步骤:
类型变量 K, 依次绑定到每个属性.
字符串字面量联合的 Keys, 包含了要迭代的属性名的集合
属性的类型.
比如下面这个例子
- type Keys = 'option1' | 'option2';
- type Flags = {
- [K in keys]: boolean
- };
Keys, 是硬编码的一串属性名, 然后这个属性的类型是 boolean, 因此这个映射类型等同于:
- type Flags = {
- option1: boolean;
- option2: boolean;
- }
典型用法 我们经常会遇到的或者更通用的是(泛型的写法):
type Nullable<T> = { [P in keyof T]: T[P] | null }
声明一个 Person 类型, 一旦用 Nullable 类型转换后, 得到的新类型的每一个属性就是允许为 null 的类型了.
- // test
- interface Person {
- name: string;
- age: number;
- greatOrNot: boolean;
- }
- type NullPerson = Nullable<Person>;
- const nullPerson: NullPerson = {
- name: '123',
- age: null,
- greatOrNot: true,
- };
骚操作 利用类型映射, 我们可以做到对类型的 Pick 和 Omit,Pick 是 ts 自带的类型, 比如下面的例子:
- export interface Product {
- id: string;
- name: string;
- price: string;
- description: string;
- author: string;
- authorLink: string;
- }
- export type ProductPhotoProps = Pick<Product, 'id' | 'author'| 'authorlink' | 'price'>;
- // Omit 的实现
- export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
- export type ProductPhotoOtherProps = Omit<Product, 'name' | 'description'>;
我们可以把已有的 Product 类型中的若干类型 pick 出来组成一个新类型; 也可以把若干的类型忽略掉, 把剩余的属性组成新的类型.
好处
keyof T 返回的是 T 的属性列表, T[P]是结果类型, 这种类型转换不会应用到原型链上的其他属性, 意味着映射只会应用到 T 的属性上而不会在原型链的其他属性上. 编译器会在添加新属性之前拷贝所有存在的属性修饰符.
不管是属性或者方法都可以被映射.
2.5 Never 类型 vs Void 类型
never 首先, never 类型有两种场景:
作为函数返回值时是表示永远不会有返回值的函数.
表示一个总是抛出错误的函数.
- // 返回 never 的函数必须存在无法达到的终点
- function error(message: string): never {
- throw new Error(message);
- }
- // 推断的返回值类型为 never
- function fail() {
- return error("Something failed");
- }
void void 也有它的应用场景
表示的是没有任何类型, 当一个函数没有返回值时, 通常 typescript 会自动认为它的返回值时 void.
在代码中声明 void 类型或者返回值标记为 void 可以提高代码的可读性, 让人明确该方法是不会有返回值, 写测试时也可以避免去关注返回值.
- public remove(): void {
- if (this.container) {
- this.mapContainer.removeChild(this.container);
- }
- this.container = null;
- }
小结
never 实质表示的是那些永远不存在值的类型, 也可以表示函数表达式或箭头函数表达式的返回值.
我们可以定义函数或变量为 void 类型, 变量仍然可以被赋值 undefined 或 null, 但是 never 是只能被返回值为 never 的函数赋值.
2.6 枚举类型
ts 中用 enum 关键字来定义枚举类型, 似乎在很多强类型语言中都有枚举的存在, 然而 Javascrip 没有, 枚举可以帮助我们更好地用有意义的命名去取代那些代码中经常出现的 magic number 或有特定意义的值. 这里有个在我们的业务里用到的枚举类型:
- export enum GEO_LEVEL {
- NATION = 1,
- PROVINCE = 2,
- CITY = 3,
- DISTRICT = 4,
- BUILDING = 6,
- FLOOR = 7,
- ROOM = 8,
- POINT = 9,
- }
因为值都是 number, 一般也被称为数值型枚举.
基于数值的枚举 ts 的枚举都是基于数值类型的, 数值可以被赋值到枚举比如:
- enum Color {
- Red,
- Green,
- Blue
- }
- var col = Color.Red;
- col = 0; // 与 Color.Red 的效果一样
ts 内部实现 我们看看上面的枚举值为数值类型的枚举类型会怎样被转为 JavaScript:
- // 转译后的 JavaScript
- define(["require", "exports"], function (require, exports) {
- "use strict";
- Object.defineProperty(exports, "__esModule", { value: true });
- var GEO_LEVEL;
- (function (GEO_LEVEL) {
- GEO_LEVEL[GEO_LEVEL["NATION"] = 1] = "NATION";
- GEO_LEVEL[GEO_LEVEL["PROVINCE"] = 2] = "PROVINCE";
- GEO_LEVEL[GEO_LEVEL["CITY"] = 3] = "CITY";
- GEO_LEVEL[GEO_LEVEL["DISTRICT"] = 4] = "DISTRICT";
- GEO_LEVEL[GEO_LEVEL["BUILDING"] = 6] = "BUILDING";
- GEO_LEVEL[GEO_LEVEL["FLOOR"] = 7] = "FLOOR";
- GEO_LEVEL[GEO_LEVEL["ROOM"] = 8] = "ROOM";
- GEO_LEVEL[GEO_LEVEL["POINT"] = 9] = "POINT";
- })(GEO_LEVEL = exports.GEO_LEVEL || (exports.GEO_LEVEL = {}));
- });
非常有趣, 我们先不去想为什么要这么转译, 换个角度思考, 其实上面的代码说明了这样一个事情:
- console.log(GEO_LEVEL[1]); // 'NATION'
- console.log(GEO_LEVEL['NATION']) // 1
- // GEO_LEVEL[GEO_LEVEL.NATION] === GEO_LEVEL[1]
所以其实我们可以通过这个枚举变量 GEO_LEVEL 去将下标表示的枚举转为 key 表示的枚举, key 表示的枚举也可以转为用下标表示.
- 3. Reference
- design pattern in typescript
- typescript deep dive
- tslint rules
typescript 中文文档
typescript 高级类型
- you might not need typescript
- advanced typescript classes and types
作者: XXXSpade
链接: https://juejin.cn/post/6844903985959190541
来源: http://www.jianshu.com/p/daf84e46376c