2021年与TypeScript愉快玩耍

1,117 阅读12分钟

本文原创:nanyifei

前言

近几年,前端领域得到了日新月异的发展,各种新技术、框架层出不穷,前端的圈子越来越大。在 Github 的官方统计中,JavaScript 已经连续多年在语言榜上拔得头筹。随着应用越做越大,业务越来越复杂,动态语言自然有其灵活性好的优点,但是同时会因为无法保证合理的类型而引起不必要的并且隐藏的 bug。并且在业务代码流转的时候,如果代码没有清晰的注释,很难阅读。

jsDoc + eslint 配合 vscode 使用能够减少许多阅读代码的不安和不适情绪,但是规则约束过于严格常常会出现因为不及时写注释而带来的大面积飘红。注释虽然可以解决一部分代码难以理解的问题,毕竟由于是静态的文本,注释无法灵活的流动

image-20210113213434694

TypeScript 在继承了 JavaScript 所有特性的基础上,给原生 js 插上了静态类型及增强的面向对象能力的翅膀,并且持续推进部分 es 提案语法成为标准。在打败了 FlowCoffeeScript 等一众对手后,成为社区的主流。在 Github 的年度语言榜上一路高歌,2020 年已经排到了第 4 位,并且还有持续进步之势。

社区内优秀的作品数不胜数,为什么要写这篇文章呢?我们受到了一种启发:在科学研究领域,有一类 paper 专门对一个研究点好的研究现状进行汇总,方便其他相关工作者能够了解该领域的最佳实践,即综述。我们想做的事情类似,把每一个前端领域的点相关的好文章整理起来,逐渐汇聚成面,以期望形成完备的知识体系,把最优秀的资源分享给大家。

这是一个开始,也是在2021年第一个月。第一篇让我们与 TypeScript 愉快玩耍。

基础篇

类型系统

从语法层面上讲,TypeScript 可以做如下理解:

TypeScript = JavaScript + 类型系统 + 增强的面向对象语法 + ES语法提案 + 编译器

总的来说,如果开发者有扎实的 JavaScript 基础,那么 Ts 上手会很快。

首先是类型系统,Ts 引入了 BooleanNumberStringSymbolArrayEnumAnyUnknownTupleVoidNever 等类型。类型系统繁多复杂,但是每种类型都有其存在的价值。

这里先推荐一些文档及文章,从中就可以窥探类型系统的秘密。

学习的一个好方法就是结合官方提供的在线编译器 Playground 与一篇详尽的文章。官方的 HandBook 自然是最全面直观的一手资料,当然 Github 及社区内也有很多优秀的文章及开源项目介绍 Ts,这里不胜列举。

在上述这些类型中,有许多需要我们额外关注的细节点:

枚举类型

我们都知道枚举类型有数字、字符串、异构及常量枚举。

  • 尤其需要注意的是,常量枚举是由 const 关键字修饰的枚举,在编译阶段被删除,也就是说 常量枚举无法编译出任何 js 代码。小伙伴们可以在 Playground 上亲自试一试。
const enum Directions {
    Up,
    Down,
    Left,
    Right
}
// 对应编译后的 js 代码只有严格模式的声明语句
"use strict";
  • 除了字符串枚举,编译后都会生成一个双向的映射关系。这就意味着如果我们使用 Object.entries 去获取枚举对象的键值对时,会有如下的结果:
enum Directions {
    Up = 0,
    Down = 1,
    Left = 2,
    Right = 3
}
const DirectionsList = Object.entries(Directions)
// [["0", "Up"], ["1", "Down"], ["2", "Left"], ["3", "Right"], ["Up", 0], ["Down", 1], ["Left", 2], ["Right", 3]] 

可以看到双向映射关系会产生对称的键值关系。如果改成字符串枚举,就会有如下的结果:

enum Directions {
    Up = '0',
    Down = '1',
    Left = '2',
    Right = '3'
}
const DirectionsList = Object.entries(Directions)
// [["Up", "0"], ["Down", "1"], ["Left", "2"], ["Right", "3"]] 

少了双向映射,就可以用枚举存储一些常见的 select 组件要求的内容了。

顶级类型和兜底类型

ts 中有两个顶级类型 anyunknown,还有一个兜底类型 never。当然 any 类型是类型系统的逃逸仓,也可以为所有类型兜底。大量使用 any 类型会带来不可控制的结果,ts 带来的静态类型检查机制无法生效,因此 TypeScript 3.0 引入了 unknown 类型。

顾名思义,unknown 是“未知”的含义,即任何明确的类型都可以赋值给 unknown 类型,而不能把 unknown 类型的变量赋值给任何一个有明确类型的变量。unknown 类型可以理解为:如果一个变量类型暂时无法确定,但是它在未来的某一时刻一定会有明确的类型,我们就可以在初始化该变量的时候,给一个 unknown 类型。因此其使用也需要明确一个原则:使用前,要对其做类型判断,否则会语法报错。我们也可以在各大框架的源代码中看到 unknown 的身影。

// Vue3 源码中关于 unknown 的使用
export function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
  
function createRef(rawValue: unknown, shallow = false) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

这里更多的细节可以参考知乎问答:写TypeScript时,什么时候用any?什么时候用unkown?有没有规律或准则?

另一方面,其实需要投入更多关注点的是兜底类型 never,直观意义上讲用于:

  • 抛出异常的函数
  • 无限循环

实际上,never 类型是所有类型的子类型,广泛应用于工具类型中,主要用途为 discriminated union,即类型收窄,可以帮助我们写出类型更安全的代码。这里更多细节可以参考:

Object object {}

这是三种容易混淆的类型:

  • object 是比较常用的类型,区别于基本类型,用于表示非原始类型。
  • {} 是一种比较特殊的类型,不需要显式地给类型定义,当初始化一个变量为 {},ts 会自动推断该变量的类型为 {}。类似于 Object.freeze 的能力,不能直接对变量进行属性值的操作。

有关三者的详细区别请查看 一文读懂 TS 中 Object, object, {} 类型之间的区别

断言

断言的作用是让开发者告知 ts 一些相关变量的类型信息,可以分为类型断言、非空断言、确定赋值断言及 const 断言。

const 断言

在 Ts3.4 中引入了一种新的断言 const assertions,使用了该断言后,字面量不能被扩展。变量的类型被约束在当前字面量的形状上,并且属性只读。可以使用 as const<const> 两种方式来声明。

// type 的类型为 {text: string}
const type = { text: "hello" } as const;

Vue3 中,组件的 props 可以结合 PropType 进行 ts 类型声明。如果我们需要抽离一部分公共的 props 存储到 ts 文件中,在使用的时候在组件内引入,那么这个 props 对象就需要使用 const 断言来保证其只读的特性。可以在 vue-next 源码中 runtime-core/src/apiDefineComponent.ts 里关于 defineComponent 源码中也可以发现这样的特征。

// overload 4: object format with object props declaration
// see `ExtractPropTypes` in ./componentProps.ts
export function defineComponent<
  // props 是只读的
  PropsOptions extends Readonly<ComponentPropsOptions>,
  RawBindings,
  D,
  C extends ComputedOptions = {},
  M extends MethodOptions = {},
  Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
  Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
  E extends EmitsOptions = Record<string, any>,
  EE extends string = string
>(
  ...
): DefineComponent<PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE>

同时 const 断言还在 redux 的使用中有应用,具体详情请阅读:

面向类型编程

Ts 不仅仅从语法层面提供了静态类型检查机制,更是给了开发者更加广阔的空间将代码写的更加灵活,提升写代码的幸福感与安全感。引入了 ts 之后,就是以一种思维方法去开发需求,详情查看 TypeScript - 一种思维方式。如何让应用中的类型流动起来,能够复用甚至衍生,同时尽可能保证应用类型安全对于开发者而言是一个不断成长的过程。在这其中,泛型起到了至关重要的作用。

泛型

泛型是一种描述程序中存在的类型及类型之间关系的方式,泛型单独存在是没有意义的,正如其名称一样,太广泛了,因此需要配合泛型约束,通过合理的约束能够写出健壮性很强的代码。

只要是用 ts 写的第三方库中,都可以随处看到泛型的身影。而在类型编程过程中,对类型取其索引签名、做类型映射、类型修饰等等操作也如家常便饭一般常见,因此 ts 及社区提供了很多好用的工具类型,将类型编程演绎到极致。

工具类型

在工具类型中我们可以经常看到 never 类型的身影,用于帮助在条件类型中将类型收窄。

在工具类型中,我们还会经常见到 infer 关键字的身影,后面跟上一个待推断的类型。这里有一个延迟推断的思想,与 js 中回调函数的思想类似,只有 ts 获取到足够的类型信息后才能推断出类型。常见的 infer 使用在 ReturnType 这个工具类型中,用于获取函数类型的返回值类型。

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

这个工具类型可以做如下解释:如果 T 这个类型满足了 (...args: any[]) => infer R 这个函数类型的所有形状要求,而且函数的返回值类型能够被推断出来,那么函数的返回值类型为 R,否则类型推断结果为最宽泛的类型 any

有了这样子的思想,infer 还可以做更多有意思的事情,比如:在一个 Tuple 类型中,删除掉第一个位置的元素。就可以如下做:

type Tail<T extends any[]> = ((...args: T) => void) extends (a: any, ...args: infer P) => void ? P : never;
type A = [string, number, boolean];
type AT = Tail<A>
const at: AT = [1, false];

这个想法来源于我们在函数的 arguments 对象上进行提前解构,拿到第一个入参及后面的所有入参。如果类型推断可以推断出解构后的参数形状,那么就可以得到我们想要的结果。同时,这里一定要注意一个细节点,如果 Tail 这个工具类型定义为如下:

// 对比上面,我们仅仅去掉了 extends 之前这个函数形状的括号
type Tail<T extends any[]> = (...args: T) => void extends (a: any, ...args: infer P) => void ? P : never;

如上定义会一直返回一个 never 类型,原因在于如果不加括号,ts 的条件类型推断会变成 void extends (a: any, ...args: infer P) => void ,这显然是不可能满足的,因此会一直返回 never 类型。

应用篇

这一部分中,我们列举了部分社区内好的技术实践,既有在热门框架的基础应用,也有优秀的工程师的技术沉淀,部分链接可能需要梯子访问。

React

在 Class Component 中的应用

在 Hooks 中的应用

Vue

Vue2 中使用装饰器与 Class Components 方式。

Vue3 的 Composition Api 提供了更加优雅的逻辑复用能力,因此相关的逻辑工具包是学习 Vue3ts 的好资料,并且可以为简化逻辑代码,为业务赋能。

Node

Ts 在 node 端的应用主要围绕装饰器展开,即便 js 中的装饰器与 ts 中的装饰器存在诸多差异。但是有了装饰器,就可以将元编程、依赖注入的思想应用进去。

装饰器相关

框架

  • Midway
  • Nest
  • Overnight
  • Loopback

结语

TypeScript 是未来前端的大势所趋,社区活跃,用户量大,因此能够产出大量的高质量库及技术文章。本文尝试以一种综述形式,将在学习过程中阅读过的印象深刻,有深度启发的好文章分享出来,希望能够对大家的学习有一定的帮助,同时也感谢以上附录文章作者的杰出贡献。


欢迎计算机前端相关领域小伙伴加入我们,具体的招聘信息可进入公众号查看,欢迎关注。