漫谈 Typescript 研发体系建设

漫谈 Typescript 研发体系建设

TypeScript 自问世以来,由于其灵活的设计和强大的 IDE —— vscode 的支持,变得越来越普及。以下附 Github Javascript 与 TypeScript 的 PR 数量趋势图。

(黄线:Javascript;蓝线:TypeScript)

我们团队从两年前开始引入 TypeScript。TypeScript 给项目带来了诸多好处,但也可能带来额外的成本。例如广被嘲讽的 “AnyScript” 问题。

如何避免 TypeScript 的问题,发挥 TypeScript 的优势。笔者也在团队进行了漫长的建设。这篇文章就来聊一聊。

工程篇

提交时类型检查

刚开始引入 TypeScript 时,团队对 TypeScript 的认知参差补齐。为了确保项目中 TypeScript 代码质量以及类型覆盖率。我们在 pre-commit 的 hooks 中,添加类型检查。即在git commit 时,自动触发一次类型检查校验。

核心代码非常简单:

pre-commit.sh:

# 对整个项目进行完整的类型检查
TS_CHANGED=$(git diff --cached --numstat --diff-filter=ACM | grep -F '.ts' | wc -l)
if [ "$TS_CHANGED" -gt 0 ]
then
  echo '正在检查 TypeScript 类型,请稍候'
  tsc -p . || exit 1
fi

package.json 中:

"husky": {
    "hooks": {
      "pre-commit": "sh pre-commit.sh && your custom checkings"
    }
}

这里分享两个经验:

1、整个项目的类型检查是非常耗时的。所幸 TypeScript 3.4 增加了 incremental 缓存功能,类型检查可瞬间完成

2、一个常见的错误是,类型校验结果中,有 node_modules 第三方包的类型报错。解决方案是:第三方包的 types 指向 d.ts 文件(而不是 .tsx?),然后在项目 tsconfig.json 中开启 skipLibCheck 配置。

TypeScript 有一个槽点,第三方包不提供类型。时至 9102 年,该问题已然缓解很多,但仍被广泛提起。其实反过来看,即使第三方包没有提供类型,也仅仅是退回到了 Javascript 编程模式,并没有引入新的问题和成本。

tslint rule

刚开始引入 TypeScript 时,项目中低级 TypeScript 错误泛滥。例如忽略类型推导、不区分string or String、把 TypeScript 注释规范与 JsDoc 注释规范混淆。对于团队常犯的这些低级错误,利用 tslint 简洁的规则设计 API,我们贡献了较多的自研 tslint rule,大多拥有自动修复功能。如今 tslint 已经被整合到 eslint ,然而 tslint rule 依然可以在 eslint 中使用。我们通过社区的、自研的 tslint/eslint rule,有效的矫正了团队类型使用姿势。

tslint 已经整合到 eslint。我们认真挑选了社区大多数的 eslint 规则,配合 tsconfig、prettier、、工具的配置、提交时检查的配置等等,收集到了团队的工程体系 pri 中。pri 不仅能在脚手架中生成这些配置沉淀,还能让项目,在维护中,实时禁止这些配置的修改,真正做到团队代码风格统一。

prijs/prigithub.com图标

工具篇

Pont

TypeScript 最大的槽点就是类型定义成本高。通过利用 TypeScript 的类型推导能力,所有前端项目都可以分成原始类型和通过原始类型推导出来的衍生类型。而我们的类型定义成本其实只剩下了这些原始类型。

在前端这个特殊场景下,项目中所有的原始类型只会来源于业务模型和产品需求规格。产品需求规格的类型定义是少量的,业务模型才是大头。而业务模型的类型,在拥抱静态类型的后端代码中,其实早已仔细定义过一份。如果前端可以与后端,共享接口定义、返回数据类型的定义,那么前端的类型定义成本将大大降低!

2018 年,我开发了一个前端联调神器 pont,并由广大 Github 开发者共同完善。

pont 通过 Swagger 等接口文档工具,获取后端的接口、实体类的数据结构,然后转换为类型完美的前端接口层代码和业务模型实体类代码。自 pont 诞生后,团队成员再也没有写过一行接口请求的代码。pont 详细介绍:

沉浸式接口开发体验

  • 接口搜索。Controller 名及接口方法名与后端完全一致。
  • 接口开发。屏蔽接口调用逻辑、完备的提示与校验、可关联跳转到自动生成的 mocks 数据当中。

联调维护

  • 接口变更通知
  • 更新接口后,前端需要更改的代码将自动提示。

我们团队在使用 Pont 后,类型覆盖率大大提升,真正把 TypeScript 的价值发挥到最大。我们也非常欢迎读者可以一起完善 Pont,让 Pont 更加强大。

pontgithub.com图标

框架篇

团队自 2015 年,便开始使用 Redux 数据流框架。在 Redux 中,有自定义的 Action 形态(自定义Middleware)、隐式的 bindDispatch、hack 的 combineReducer。要达到类型完美匹配是非常困难的。好在 TypeScript 有强大的类型推导能力,强大到 TypeScript 的类型本身也是可编程的。

例如,覆盖 combineReducers 类型,推导出 Redux 全局状态树类型:

export function combineReducers<S, A extends Action = Action>(
  reducers: ReducersMapObject<S, A>
): Reducer<S, A>;

/** 根据 Reducer Map 返回 全局 State */
export type ReturnState<ReducerMap> = {
  [key in keyof ReducerMap]: ReducerMap[key] extends (state: any, action: any) => infer R ? R : any
};

export type GlobalState = ReturnState<typeof reducers>;

着眼于 TypeScript 类型推导的能力,我在 2017 年,制定了 iron-redux 规范。该 repo 主要由类型方法和代码规范组成,它给我们带来了如下便利:

  • 解决 Redux 代码冗余;让 React + Redux 组合是,类型完美契合。
  • 自动推导全局的 Redux 状态树类型。
  • 在 Reducer switch case 中,自动推导每个 case 下的 payload 类型。
  • 专属的 vscode 插件支持。
  • 300 行源码,零依赖。
nefe/iron-reduxgithub.com图标

规范篇

FP 与 OOP

Redux 是一个拥抱 FP 的框架。纯函数的概念,让模块更可靠、架构逻辑更清晰,极大降低了项目复杂度。众所周知,OOP 的方法,是天然 mutable 的(this.xx = xx;),这与纯函数的概念天然相悖。此外,在 Redux 中使用 OOP,plain object 需要构造为对象,以调用实例方法;对象又需要转换为 plain object,以便在 Redux 中存储。这也是极为不便的。

然而 OOP 的优势也很明显:1、在业务模型复杂的时候,OOP 把数据结构和处理数据结构的方法组织在一起。比起 FP 散乱陈列的方法更为清晰。是人类更加容易理解的代码组织方式。2、OOP 有丰富的、成熟的、好用的设计模式,团队同学也对这些设计模式梗熟于心。3、最重要的是,OOP 相比于 FP,更容易发挥 TypeScript 的优势。

为了能够方便的使用 OOP,又避免 OOP 在 FP 中使用的问题。我在团队推行了如下 OOP 使用规范:

1、class 声明属性时,如业务模型有默认值,应当声明默认值,避免重复定义默认值模型;默认值可以推导属性类型,不再重复声明类型。

2、将实例方法,改造为静态方法:

  • 去掉实例方法中的 this,把实例对象作为第一个参数
  • 静态方法是纯函数

例如:

class Apple {
  /** 数量 */
  count = 0;
  
  /** 单价 */
  price = 2.0;
  
  static getPrice(apple: Apple, bagPrice: number) {
    return apple.count * apple.price + bagPrice;
  }
}

class Shopping {
  apple = new Apple;
  
  peopleNum = 3;

  static getPrice(shopping: Shopping) {
    return Apple.getPrice(shopping.apple + 0.1) / shopping.peopleNum;
  }
}

思想篇

TypeScript 是在 Javascript 上附上类型,以在开发时、编译时增加编程体验、稳定性。如何理解呢?

1、如果一个数据、方法、模块,类型定义成本高,却不被调用,那么它的类型定义就是毫无意义的。这个时候果断加上 any,不要有心里负担。

2、类型代码在编译后会消失,如果仅仅调整代码类型,对代码运行时逻辑不会有任何变更。

举一个实际工作中的例子。团队中有一个国际化解决方案 kiwi ,kiwi 提供了一个 vscode 插件,将前端代码中的产品中文文案自动提取,组织到一个大 Map 对象中,把原文案替换为 I18N.a.b.c(文案访问路径)。kiwi 再提供命令,将大 Map 对象的文案,自动送翻、机翻为不同语言的文案。

项目接入 kiwi 后,我在 review 接入代码时,发现 I18N 是一个 any 类型,于是只增加了一行代码:

const I18N = xx as typeof Map & I18NAPI;

这样所有访问 I18N 的文案都有了类型,在项目中检测出了十几处路径拼写错误。我们在实际使用 TypeScript 时,忽略运行时的实际逻辑,牢记 TypeScript 是用来服务我们的编程体验,代码可靠性的,会让我们对 TypeScript 使用得更加得心应手。

结尾

随着客户端设备越来越好,前端项目也越来越庞大和复杂,相信 TypeScript 也会越来越普及。团队技术氛围好,大神多,妹纸多,业务扩张,前途无量!热烈欢迎广大读者投简历~

招聘主页:github.com/nefe/Hiring

阿里 DT 前端 - 招聘主页github.com图标

编辑于 2019-10-12

文章被以下专栏收录