从 JS 迁移到 TS(写给初学者的笔记)

相比于强类型的语言来说, js非常动态, 它的弱类型特点既带来了运行时超高的灵活性, 但是也带了开发时的不确定性, 所以后来微软设计了ts, 在开发阶段对代码进行类型校验, 偏向于强类型, 提供开发时的安全性, 在编译后移除所有的类型信息变回到js,保留了运行时的弱类型灵活性特点.

本篇笔记针对是从jsts的转变过程, 包含个人的一些建议和想法, 更多的是想帮助你平滑的学习和使用ts, 此文并非ts语法教科书, 如果你只对语法感兴趣的话可以直接跳转到「正式的学习资料」这个小节.

# 从「入门」到「跑路」

初期会痛苦, 中期会习惯, 后期会舒服.

# 初期

编辑器不断地给你的代码打上红线, 提示这个地方类型有问题, 而你又挠破脑袋觉得「WFT」要怎么写, 搜索也不知道该搜什么关键词合适, 这个时候痛苦, 同时编码效率也会下降, 以前一天能做完的任务, 现在应该要一天半(多出来的半天是你在挠头弄明白这个地方应该是什么类型).

  1. 使用别人已经编辑好的一份比较简单的tsconfig.json配置文件, 暂时不要尝试自己从头配置(相信我, 完全搞懂配置文件真的让人头大).

  2. 只接触基本语法和用法string/boolean/interface/type等, 解决不了的地方如果着急可以先any, 等更熟悉一点之后再回头把any一点点改掉(当然了, 我们都不希望看到一个新的anyScript出现).

  3. 从比较纯粹独立的文件用起, 例如你常写的utils函数文件, 复杂的业务逻辑文件先不用动, tsjs是可以在一个项目中共存的, 所以不用着急, 慢慢来.

  4. 问使用过ts的人, 对于简单的问题, 他们的答案比搜索引擎来的快.

# 中期

你习惯了为自己写下的函数或者变量声明类型, 编码效率也恢复正常, 因为你已经驾轻就熟, 同时配合代码提示自动补全以及类型校验带来的安全感, 你感觉自己无所不能.

  1. 是时候「浪子回头」把之前的any一点点改为更具体的声明了, 良好的声明能够给你编写和重构代码代码带来安全感, 只要编辑器点头了, 那就代表几乎不会有低级错误了(比如english写成engilsh, 或者这个对象居然还会undefined?).

  2. 使用更高级的用法, 比如类型守卫Type Guard/<T>泛型/infer推断, 开始自己写一些类型推断函数来让你的代码更整洁高效. (ts本身也可以变得很复杂, 你甚至可以用它来做逻辑运算, 在leetcode上还有题目可以刷, 但是无需担心, 在你日常的使用中接触不到那种复杂度).

  3. 了解tsconfig.json的配置, 它决定了你写的ts代码会被如何编译成js, 你可以把它类比为webpack的配置文件(不过比webpack还是要简单多了, 毕竟有webpack配置工程师, 但是不会有ts配置工程师).

# 后期

日常使用已经没有问题, 甚至有了点强迫症, 不上ts的代码能看?

(别找建议了, 没有, 因为我学的还不够, 反而欢迎给我点建议.)

# 渐进性

渐进性体现在好几个方面, 可以让ts的学习曲线平滑.

# 不需要一次性把所有.js文件都改为了.ts文件

tsjs的超集, 也就是说所有的js代码都是有效的ts代码, 文件后缀名的修改对你的代码运行不会造成任何影响; 另外二者是可以共存一段时间的, 从小的地方慢慢改起, 比如工具函数, 改造它不会对你的业务造成影响, 当有了信心之后再改剩下的文件.

# 不需要写出所有的类型声明, ts是智能的

它会自动静态分析你的代码自动推断出合适的类型. 你可以把以下代码拷贝到你的编辑器中, 保存为.ts后缀文件, 然后鼠标hover变量或者函数, 看看有什么提示(剧透: 它很聪明的!).

const a = 1;
// 1 是数字, 你将一个数字复制给变量a, 于是ts就推断出变量a应当是一个 number (数字)类型的变量, 很聪明吧.
// 不需要你使用 const a: number = 1 这种方式来手动告诉ts a的类型

const b = [1];
// [1] 是一个数组, 它包含一个数字 1, 于是ts就推断出变量b应当是一个包含数字的数组类型.

const c = [1, 'hello'];
// [1, 'hello'] 是一个数组, 它包含一个数字 1 和一个字符串 'hello', 于是ts就推断出变量c应该是一个包含数字或者字符串的数组: (number | string)[]

function add(a: number, b: number) {
  return a + b;
}
// 我们这里虽然声明了函数入参a和b的类型, 但是并没有声明函数返回值的类型, 但是你hover函数看看它是不是准确的提示了你返回值是 number 类型, number + number 得到的当然是 number, ts果然很聪明.

如果你思考的多一点, 就会有一个疑惑, 在上面的示例中const b = [1]自动推断的结果为什么是number[], 而不是[number](这两者是有区别的, 前者只是说这是仅包含数字的数组, 后者则限定了是仅包含一个数字的数组), 这是因为ts有一套自己的规则(the best common type)来决定最后推断的结果, 这是经过思考和实践后得到的一个比较合适的规则, 在日常使用中能为我们带来最大的便利.

如果你发现它自动推断的结果不是你想要的, 比如虽然初始化时const b = [1], 但是实际上b之后有可能会被b.push('hello'), 也就是b实际是可能存在其他类型值的, 那么我们就需要const b:(number | string)[] = [1]手动声明b的类型, 这就是告诉ts: 你别忙活啦, 我比你懂代码, b的类型应该是 (number | string)[]. 之后ts就会按照你声明的类型来对代码进行校验.

# 那么什么时候需要手动声明类型呢?

# 不需要一定去把源码改为ts

有很多代码不在我们的控制范围内, 比如社区一些比较早期的npm包(lodash等), 或者我们内部的一些npm包, 希望在引入使用的时候能够得到包里面api的代码提示, 但是又不好去改源码, 我们就可以单独编写与之适配的声明文件来和包本身配合使用

社区里存在大量@types/xxx(例如@types/lodash)的包, 里面其实就是一份声明文件, 指定与之适配的包有哪些api, 这种包对运行时没有任何影响, 也不会打包到编译结果中, 所以都是以--save-dependencies的方式安装的.

如果源码是js又没有现成的与之适配的@types/xxx类型包, 那么我们也可以在项目中使用declare方式手动为其编写api声明, 来让使用的时候可以有代码提示和校验.

需要注意的是: 这种情况不是最优解, 而是在无法变更源码的情况下做出的适配妥协, 它的缺点是需要和源包配套使用, api的声明和实际实现保持一致, 如果某次源包做了升级, 修改了某个api, 那么声明包也需要跟着升级修改api的声明, 否则就会出现声明和实现不一致的情况(错误的ts提示比没有提示更糟糕), 所以在条件允许的情况下对源工程进行一下升级吧, Let's TS it!.

# 正式的学习资料

TypeScript于2012年10月首次发布0.8版本, 经过了近10年的迭代和发展到现在的4.5版本, 也经过了和flow的争锋(比如尤雨溪直呼「压错宝」), 如今已经是大势所趋, 也形成了一个很繁荣和稳定的社区.

看完了上面很多我个人的理解和想法, 你最关心的应该是这个章节, 不过你可能会有点失望, 因为社区里文章教程已是汗牛充栋, 我再写也是关公门前耍大刀不会写的比他们更好, 所以我列出了学习过程中收藏的比较好的文章供大家查阅.

  1. 深入理解 TypeScript, 这是 《TypeScript Deep Dive》 的中文翻译版, 初学者额从概览开始看起会比较好入门, 里面也有一些高级的用法可供查阅, 建议通读.

    此书是 《TypeScript Deep Dive》 的中文翻译版,感谢作者 Basarat 的付出。

    如果你喜欢纸质书籍,可以通过京东或者当当,来购买此书。

  2. TypeScript 高级用法, 这篇笔记适用于在了解了基础用法入门后查阅, 是日常使用经验的总结, 简洁实用.

    本文主要介绍 TypeScript 的高级用法,适用于对 TypeScript 已经有所了解或者已经实际用过一段时间的同学,分别从类型、运算符、操作符、泛型的角度来系统介绍常见的 TypeScript 文章没有好好讲解的功能点,最后再分享一下自己的实践经历。

    作者:字节前端 链接:https://juejin.cn/post/6926794697553739784 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  3. tsconfig.json工程配置文件, 这篇笔记适用于tsconfig.json配置工程师(开玩笑, 暂时没有这个职位), 详细讲解了各个配置字段的函数, 方便理解和查阅, 完整的官方索引配置可以查看这里.

    tsconfig.json 是 TypeScript 项目的配置文件,如果一个目录下存在一个 tsconfig.json 文件,那么它意味着这个目录是 TypeScript 项目的根目录。TypeScript 编译器编译代码之前,首先读取这个配置文件,并根据其中的属性来设置 TypeScript 项目的编译参数。