2021年与TypeScript愉快玩耍

本文原创:nanyifei

前言

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

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

1.jpg

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • Any application that can be written in Javascript, will e...
    前端精阅读 462评论 0 3
  • 官方handbook[https://www.typescriptlang.org/docs/handbook/i...
    ShoneSingLone阅读 577评论 0 0
  • 使用ts写React代码写了将近三个月,从刚开始觉得特别垃圾到现在觉得没有ts不行的一些实践以及思考。 如果按部就...
    小哪吒阅读 31,048评论 1 9
  • 2019 9月 [工具] TreeJS 在线编辑器: https://threejs.org/editor/[ht...
    halber阅读 1,997评论 0 2
  • 一、简介 1.1 什么是 TypeScript TypeScript 是 JavaScript 的一个超集,主要提...
    _ihhu阅读 1,199评论 0 2