一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情。
本文的翻译于<<Effective TypeScript>>, 特别感谢!! ps: 本文会用简洁, 易懂的语言描述原书的所有要点. 如果能看懂这文章,将节省许多阅读时间. 如果看不懂,务必给我留言, 我回去修改.
了解类型系统
本章主要的带大家理解和掌握ts的类型系统
技巧6: 用你的IDE(代码编辑平台)来查询,探索ts的类型系统
IDE可以选择webStorm或者vscode等
我们可以用IDE学习类型系统:
-
查看变量的类型. 鼠标悬浮到变量名上, 就能显示该变量的类型
-
查看函数的类型:
这里需要注意的: 类型系统可以自动推断函数的返回值类型, 当然你也可以自己设置返回值类型
-
查看if分支内变量的类型
类型系统可以根据if条件,来修订变量类型
-
查看对象属性值的类型:
-
在操作链中查看类型:
除了上面的方法, 我们还可以通过报错来学习类型系统:
function getElement(elOrId: string|HTMLElement|null): HTMLElement {
if (typeof elOrId === 'object') {
return elOrId;
// ~~~~~~~~~~~~~~ 'HTMLElement | null' is not assignable to 'HTMLElement'
} else if (elOrId === null) {
return document.body;
} else {
const el = document.getElementById(elOrId);
return el;
// ~~~~~~~~~~ 'HTMLElement | null' is not assignable to 'HTMLElement'
}
}
第一个报错: 第一个判断条件本应该筛选elOrId为HTMLElement, 但是奇怪的是:在js中, typeof null 也是 'object', 所以 elOrId可能为 HTMLElement, 也可能为null.
第二个报错: document.getElementById 可能返回null 也可能返回HTMLElement
除了上面的好处, ts的类型系统还可以帮你查询库里面的变量, 函数类型等.
例如: 代码里面有fetch函数, 你想多了解了解:对准fetch函数右键,选择Go to Definition
你就可以查阅fetch函数的参数和返回值:
declare function fetch(
input: RequestInfo, init?: RequestInit
): Promise<Response>;
同样的方法, 你可以查阅RequestInfo的类型申明.
type RequestInfo = Request | string;
同样的方法, 你可以查阅Request的类型申明.
declare var Request: {
prototype: Request;
new(input: RequestInfo, init?: RequestInit): Request;
};
同样的方法, 你可以查阅RequestInit的类型申明.
interface RequestInit {
body?: BodyInit | null;
cache?: RequestCache;
credentials?: RequestCredentials;
headers?: HeadersInit;
// ...
}
技巧7: 把类型看成值的集合
最好把类型看做值组成的集合
什么意思? 举个例子:
number 可以考虑成由number值组成的集合, 包括有42, -37.25 但是不包括 'canada'.
当你设置ts的配置strictNullChecks为true时, null 和 undefined 也不属于number集合.
那什么是最小的集合?
空集合, 对应的type是never. 不能将值分配给, 类型为never的变量, 例如:
const x: never = 12;
// ~ Type '12' is not assignable to type 'never'
下一个最小的集合是什么?
只包含单个值, 例如:
type A = 'A';
type B = 'B';
type Twelve = 12;
你可以联合单个值的类型:
type AB = 'A' | 'B';
type AB12 = 'A' | 'B' | 12;
在ts错误中常出现一个单词: assignable, 用值的集合理解, 这个单词的意思: 属于xxx的成员, 或者说属于xxx的子集:
const a: AB = 'A'; // OK, value 'A' is a member of the set {'A', 'B'}
const c: AB = 'C';
// ~ Type '"C"' is not assignable to type 'AB'
'A' 就是 type AB 的成员, 或者说子集
'C' 就不是
上述类型作为集合很好理解, 因为他们集合中的元素个数是有限的, 但是实际工作中, 遇到大部分的类型集合的元素个数都是无限的 例如:
interface Identified {
id: string;
}
同时把类型考虑成集合, 那么类型也有集合中常见的操作: 交、并。 用符号 &, | 来表示. 看下面的集合:
interface Person {
name: string;
}
interface Lifespan {
birth: Date;
death?: Date;
}
type PersonSpan = Person & Lifespan;
用&来计算Person , lifespan的交集. 第一眼看,发现Person, lifespan 没有相同属性, 那么他们的交集PersonSpan应该是空集.
但是要记住: 类型是值的集合, 而不是属性的集合.
其实:
const ps: PersonSpan = {
name: 'Alan Turing',
birth: new Date('1912/06/23'),
death: new Date('1954/06/07'),
}; // OK
为什么呢?
根据类型的兼容原则(见技巧1-技巧5), 属于PersonSpan的变量, 一定属于Person和属于Lifespan.
所以根据交集的定义PersonSpan = Person & Lifespan;
keyof可以得到类型的所有属性. 实际上:
keyof (A&B) = (keyof A) | (keyof B)
keyof (A|B) = (keyof A) & (keyof B)
所以:
keyof (Person & Lifespan) = keyof(Person) | keyof(Lifespan) = (name) | (birth, death) = (name,birth, death)
keyof (Person | Lifespan) = keyof(Person) & keyof(Lifespan) = (name) & (birth, death) = 空
但其实我们不常用 | 符号, 我们更到用extends.
interface Person {
name: string;
}
interface PersonSpan extends Person {
birth: Date;
death?: Date;
}
extends 什么意思? 也就是'属于xxx','xxx的子集'.
再来看一个例子:
interface Vector1D { x: number; }
interface Vector2D extends Vector1D { y: number; }
interface Vector3D extends Vector2D { z: number; }
可以说Vector3D 是 Vector2D的子集, Vector2D 是Vector1D的子集. 也可以理解为: Vector3D 继承了Vector2D所有的属性.
当然extends在泛型中显示为约束.如:
function getKey<K extends string>(val: any, key: K) {
// ...
}
K extends string意思是K被约束为string. 所以有:
getKey({}, 'x'); // OK, 'x' extends string
getKey({}, Math.random() < 0.5 ? 'a' : 'b'); // OK, 'a'|'b' extends string
getKey({}, document.title); // OK, string extends string
getKey({}, 12);
// ~~ Type '12' is not assignable to parameter of type 'string'
最后: 并不是所有值的集合都能对应到ts的类型,例如没有一个ts的类型表示所有整数. 不过有时候你可以用Exclude排除部分类型:
type T = Exclude<string|Date, string|number>; // Type is Date
type NonZeroNums = Exclude<number, 0>; // Type is still just number