先问一个问题,JavaScript有几种数据类型?


numberstringbooleannullundefinedsymbolbigintobject


其中 bigint 是 ES2020 新增的数据类型,而早在 TS3.2 时便成为 TS 的标准,其实还有好多 ES+ 标准是 TS 率先提出的,可见 TS 在很多方面是走在了 ES 前列。


TypeScript又新增了多少种数据类型?


anyunknownenumvoidnevertuple...


其实 TypeScript 更重要的是通过 interface 和 type 赋予了用户自定义数据类型的能力,使数据在流转的过程中始终能轻易被用户掌握。



Bug 绞杀者


TypeScript 可以让我们的程序写的更健壮,并且更好维护,丰富的代码的提示功能也能够提高我们的开发效率以及降低协作成本,并帮助我们在程序的编译阶段检查出许多因为类型的原因导致的低级错误。


那么我们先来看下 JavaScript 项目最常见的十大错误:

TypeScript 原来可以这么香?!_v8



这些低级的错误是不是耳熟能详?为了解决这些问题,占用了我们大量的 debug 和 google 的时间。程序员最烦的两件事:一件是自己写完代码还要写注释文档,一件是别人的代码没留下任何注释文档。


在我们的日常开发中,很常见的是调用或者修改别人写的函数。但是如果别人的代码没留下任何注释的话,我们就要硬着头皮去看里面的逻辑。


假如我们优化了一个底层类库的参数类型,而不知道有多少处引用,在提交代码前,是不是内心在打鼓,心里没底呢?毕竟我们都不想被杀了祭天吧...


笔者前不久就因为一个很小的问题 debug 了很长时间,我们来看一下,因内部代码所以经过脱敏处理。

TypeScript 原来可以这么香?!_v8_02


因为 JavaScript 太灵活了,它允许你对参数做任何操作,所以之前同学写的时候直接将 userId 挂载到入参 query 上面,后面用到处解构也就有了相应的 userId 属性。但是笔者对这部分业务不熟悉,以为入参 query 不会改变,所以 debug 了好久。


而假如我们使用的是 TypeScript ,我们会定义好入参 query 的结构,为 query 添加 userId 属性的时候就能有报错提示,可以规避我们以前的写法,换成更健壮性的写法。


另外 TypeScript 还能实现自文档化,使后面维护的同学也能轻松的接手。

TypeScript 原来可以这么香?!_javascript_03


我们可以通过​​/** */​​来注释 TypeScript 的类型,当我们在使用相关类型的时候就会有注释的提示,这个技巧可以帮助我们节约翻文档或者跳页看注释的时间,在团队内合理的推广使用,能够极大的提高我们的开发效率。


优化程序性能


合理的使用 TypeScript ,再凭借 V8 引擎可以帮助我们极大的优化程序的性能。


在 V8 引擎下,引入了 TurboFan 编译器,它会在特定的情况下进行优化,将代码编译成执行效率更高的 Machine Code,这个编译器虽然不是 JavaScript 必须的,但是却能够极大的提高代码执行效能。


TypeScript 原来可以这么香?!_v8_04


我们知道, JavaScript 代码首先会解析为抽象语法树(AST),然后会通过解释器或者编译器转化为 Bytecode 或者 Machine Code。其中 Ignition 负责将 AST 转化为 BytecodeTurboFan 负责编译出优化后的 Machine Code,并且 Machine Code 在执行效率上优于 Bytecode

TypeScript 原来可以这么香?!_typescript_05


那么问题来了,什么情况下的代码会编译为 Machine Code 呢?JavaScript 是一门动态类型语言,并且有一大堆的隐式类型转换规则,比如数字相加、字符串相加、对象和字符串相加等等。这样的情况也就势必导致了内部要增加很多的判断逻辑,降低运行时的效率。

function test (x) {
return x + x;
}

test(1)
test(2)
test(3)
test(4)

对于上面这段代码来说,如果一个函数被多次调用并且参数一直传入 number 类型,那么 V8 引擎就会认为该段代码可以编译为 Machine Code,因为我们固定了类型,不需要再执行很多判断逻辑了。


但是一旦我们传入的参数类型改变,那么 Machine Code 就会被 DeOptimized Bytecode,这样就会造成性能上的损耗。所以如果我们希望代码能尽可能多的编译为 Machine Code 并且 DeOpimized 的次数减少,那么我们就应该尽可能的保证传入的类型一致。


但是你可能还有一个疑问,优化前后的性能提升到底是怎么样的呢?有什么数据支撑么?

const v8 = require('v8-natives');
const { performance, PerformanceObserver } = require('perf_hooks')

function test(x, y) {
return x + y
}

const obs = new PerformanceObserver((list, observer) => {
console.log(list.getEntries())
observer.disconnect()
})
obs.observe({ entryTypes: ['measure'], buffered: true })

performance.mark('start')

let number = 10000000

// 不优化代码
v8.neverOptimizeFunction(test)

while (number--) {
test(1, 2)
}

performance.mark('end')
performance.measure('test', 'start', 'end')


我们接下来使用 performance 这个 API 测量一下代码的执行时间,这个 API 经常用于一些性能测试,还可以用来测量各种网络连接中的时间消耗,并且也可以在浏览器中使用。

TypeScript 原来可以这么香?!_typescript_06


我们实际运行一下代码发现,经过优化后的代码执行时间只需要 10ms,但是不优化的代码却是前者的十二倍,高达了 124ms


在这个案例中,我们能够看到 V8 的性能优化十分强大,只需要我们符合一定规则书写代码,引擎底层就能帮助我们自动优化代码。


那么为了让 V8 优化代码,我们要尽可能的保证传入参数的类型一致,而这,也正是 TypeScript 带给我们的好处之一,借助 TypeScript,可以强迫我们思考定义好每一处的变量类型,让每一处的变量类型都做到最小可控。使 V8 可以自动将我们的代码优化成 Machine Code。


所以我们可以设想一下,未来怎么样凭借 TypeScript,让 V8 进一步得到优化。

  • 1、使用 TypeScript 编程,遵循严格的类型化编程规则,摒弃 AnyScript。

  • 2、构建的时候将 TypeScript 直接编译为 Bytecode,而不是 JavaScript 文件,这样运行的时候就省去了 Parse 以及生成 Bytecode 的过程。

  • 3、运行的时候,需要先将 Bytecode 编译为对应 CPU 的汇编代码。

  • 这样由于采用了类型化的编程方式,有利于编译器优化所生成的汇编代码,省去了很多额外的操作。


未来的潮流


很多的前端底层库都在从 JavaScript 向 TypeScript 迁移,像我们熟悉的 Angular 和 Vue3 已经全面用 TypeScript 重构代码,在 ECMAScript 标准推出静态类型检查之前,TypeScript 是解决当下问题的最佳实践。


stackoverflow 统计的2020年最受开发者喜欢的语言,TypeScript 已排到第二名。

TypeScript 原来可以这么香?!_v8_07


但是很多同学一开始可能不太喜欢使用 TypeScript,人们在接触一个新事物的时候往往会出现本能的抗拒,因为不太确定这个新的事物能够带给我们什么开发体验的提升。


可能有些同学抱着试试看的态度去尝试使用了一下,但却发现这个东西巨难用,常常代码死活编译不过去,各种类型不匹配。一些底层类库的不完善,没有很好的支持 TypeScript 也会增加我们开发的上手难度。


这也难怪,因为 JavaScript 是一门动态弱类型语言,对变量的类型非常宽容,而且不会在这些变量和它们的调用者之间建立结构化的契约,JavaScript 带给我们极大的灵活性。

TypeScript 原来可以这么香?!_javascript_08

一想到以前快乐的时光,可能有的同学直接弃疗,重新回到 JavaScript 的怀抱;可能有的同学转而去写 AnyScript,遇事不决上 any,但这是非常差的编程习惯!假如我们写 AnyScript,我们就失去了 TypeScript 的意义,为此我们还要多写好多的冗余代码,影响开发效率。


其实使用 TypeScript 进行开发,理论上我们99.99%的情况都不应该使用 any,当我们想要使用 any 的时候,就多去思考,查阅资料,仔细推敲,只要我们能够不断忍住不写 any,多写 TypeScript,写到如臂指使,后面你会发现自己的开发效率在不断提升。


实用小技巧


1. 巧用 keyof


假如我们要实现一个 getValue 函数,作用是根据传入的 object 和 key 获取 object 上 key 所对应的属性值 value,一开始的 JavaScript 版本可能是这样的。

const data = {
a: 3,
hello: 'world'
}
function getValue(o, name) {
return o[name]
}

可以看到这就是 JavaScript 的弊端,我们对传入的 o 和 name 都没有约束,对这一切都是未知的,人的恐惧就源自对未知事物的不确定性。尤其如果这是一个底层类库,我们不知道有多少人去调用,那这个风险就更加增大了,所以我们可能用 TypeScript 这样去改造它。

function getValue(o: any, name: string) {
return o[name]
}

但这样写貌似也有很多问题,函数返回值的类型变成了 any,失去了 TypeScript 类型校验的功能,让类型重新变的不可控,name 类型固然是 string,但还是太宽泛了,实际上,name 的值只能是o的键的并集,而且如果我们将 name 拼写错误,TypeScript 也不会帮我们报错提示。这个时候我们可以使用 keyof 结合泛型来加强 getValue 函数的类型功能。

function getValue<T extends object, K extends keyof T>(o: T, name: K): T[K] {
return o[name]
}


2. 接口智能提示

interface Seal {
name: string;
url: string;
}
interface API {
"/user": { name: string; age: number; phone: string };
"/seals": { seal: Seal[] };
}
const api = <URL extends keyof API>(url: URL): Promise<API[URL]> => {
return fetch(url).then((res) => res.json());
};


借助泛型以及泛型约束,我们可以实现智能提示的功能,不光接口名可以智能提示,接口返回也可以智能提示。当我们输入 api 的时候,其会自动将 API interface 下的所有 key 提示给我们,当我们输入某一个 key 的时候,其会根据 key 命中的 interface 定义的类型,然后给予类型提示。这在统一接口管理方面有很大的用处,可以帮助我们面向接口编程。

TypeScript 原来可以这么香?!_typescript_09

TypeScript 原来可以这么香?!_v8_10

TypeScript 原来可以这么香?!_typescript_11


3. 巧用类型保护


interface User {
name: string;
age: number;
occupation: string;
}
interface Admin {
name: string;
age: number;
role: string;
}
export type Person = User | Admin;
export const persons: Person[] = [
{
name: 'Max Mustermann',
age: 25,
occupation: 'Chimney sweep'
},
{
name: 'Jane Doe',
age: 32,
role: 'Administrator'
},
{
name: 'Kate Müller',
age: 23,
occupation: 'Astronaut'
},
{
name: 'Bruce Willis',
age: 64,
role: 'World saver'
}
];
export function logPerson(person: Person) {
let additionalInformation: string;
if (person.role) {
additionalInformation = person.role;
} else {
additionalInformation = person.occupation;
}
console.log(` - ${person.name}, ${person.age}, ${additionalInformation}`);
}
persons.forEach(logPerson);

TypeScript 原来可以这么香?!_javascript_12我们可以看到,当我们定义了两种 Person 类型:User 和 Admin,而在使用的时候是比较宽泛的 Person,那我们就不能直接使用 User 或者 Admin 的特有属性 role 或者 occupation。因为 TypeScript 没有足够的信息确定 Person 究竟是 User 还是 Admin。


一种方法是使用类型断言,显示的告诉 TypeScript,person 就是 Admin 类型或者就是 User 类型,但是这样做一方面不够优雅,要在每一处都加上断言;另一方面滥用断言也会让我们的代码变得不可控,不能让 TypeScript 帮助我们进行合理的类型推断。像双重断言可以规避掉 TypeScript 的类型检查机制也是与 any 一样,要尽可能去避免的。

TypeScript 原来可以这么香?!_javascript_13


正确的做法是使用类型收缩,比如使用 is,in,typeof,instanceof 等,使得 TypeScript 能够 get 到当前的类型,假如 person 上面有 role 属性,TypeScript 就可以推断出 person 就是 Admin 类型,创建类型保护区块,在当前的代码块按照 Admin 类型处理,代码也简洁了很多。

TypeScript 原来可以这么香?!_javascript_14

同样是这个例子,我们再改造一下,通过两个函数来判断 person 的具体类型是 Admin 还是 User。但是很不幸,TypeScript 依然不能很智能的知道 person 在第一个代码块里是 Admin 类型,在第二个代码块里是 User 类型。

TypeScript 原来可以这么香?!_javascript_15


这个时候我们就要改造一下 isAdmin 和 isUser 的函数返回,创建用户自定义的类型保护函数,显式的告诉 TypeScript,函数返回为 true 时,指定 person 的类型确定为 Admin 或者 User,这样 TypeScript 就知道 person 的确定类型了,这就是类型位词。

TypeScript 原来可以这么香?!_typescript_16


4.常用的高级类型


这其实涉及到了类型编程到概念,简而言之,我们平时写代码是对值进行编程,而类型编程是对类型进行编程,可以利用 keyof 对属性做一些扩展,省的我们要重新定义一下接口,造成很多冗余代码。


这些高级类型在日常编程中有非常广泛的使用,尤其 Partial 可以将所有的属性变成可选的,如在我们日常的搜索逻辑,我们可以根据单一条件搜索,也可以根据组合条件搜索。Omit 可以帮助我们复用一个类型,但是又不需要此类型内的全部属性,当父组件通过 props 向下传递数据的时候,可以剔除一些无用的类型。


Record 也是一个比较常用的高级类型,可以帮助我们从 Union 类型中创建新类型,Union 类型中的值用作新类型的属性。当我们拼写错误,或者漏写一些属性,或者加入了没有预先定义的属性进去,TypeScript 都可以给我们很友好的报错提示。


type Partial<T> = {
[P in keyof T]?: T[P];
};
type Required<T> = {
[P in keyof T]-?: T[P];
};
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
type Exclude<T, U> = T extends U ? never : T;
// 相当于: type A = 'a'
type A = Exclude<'x' | 'a', 'x' | 'y' | 'z'>
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
type Record<K extends keyof any, T> = {
[P in K]: T;
};

interface User {
id: number;
age: number;
name: string;
};
// 相当于: type PartialUser = { id?: number; age?: number; name?: string; }
type PartialUser = Partial<User>
// 相当于: type PickUser = { id: number; age: number; }
type PickUser = Pick<User, "id" | "age">
// 相当于: type OmitUser = { age: number; name: string; }
type OmitUser = Omit<User, "id">

type AnimalType = 'cat' | 'dog' | 'frog';
interface AnimalDescription { name: string, icon: string }
const AnimalMap: Record<AnimalType, AnimalDescription> = {
cat: { name: '猫', icon: ' '},
dog: { name: '狗', icon: ' ' },
forg: { name: '蛙', icon: ' ' }, // Hey!
};

5.巧用类型约束

在 .tsx 文件中,泛型可能会被当作 jsx 标签

const toArray = <T>(element: T) => [element]; // Error in .tsx file.

加 extends 可破

const toArray = <T extends {}>(element: T) => [element]; // No errors.


TypeScript 还可以给一些缺乏类型定义的第三方库定义类型,找到一些没有 d.ts 声明的开源库,为开源社区贡献声明文件。


学习参考


  1. https://www.typescriptlang.org/docs/handbook/release-notes/overview.html 官方各个版本文档
  2. https://github.com/microsoft/TypeScript/projects/9 官方讨论
  3. https://github.com/microsoft/vscode VS Code 是 TypeScript 编写的,毫无疑问也是学习的好地方
  4. https://basarat.gitbook.io/typescript/getting-started TypeScript Deep Dive
  5. https://github.com/typescript-exercises/typescript-exercises TypeScript 优秀的练习题