从React迁移到TypeScript:忍受了15年的JavaScript错误从此走远

2020 年 6 月 01 日

从React迁移到TypeScript:忍受了15年的JavaScript错误从此走远

本文最初发布于 executeprogram 网站,经网站授权由 InfoQ 中文站翻译并分享。这篇译文是第三方翻译版本,未经原文作者审核。


Beta 版的 Execute Program 是用 Ruby 和 JavaScript 编写的。之后,我们分几步将整个应用完全移植到了 TypeScript 上。本文介绍的是移植的第一步,也就是前端的部分。


在 Execute Program 的原始 JavaScript 前端中,我经常会犯一些小错误。例如,我会将错误的 prop 名称传递给 React 组件,或者遗漏某个 prop,抑或传递错误的数据类型。(Prop 是作为参数发送到 React 组件的数据。组件将一些 props 传递给自己的某个子组件,以此类推,这是很常见的。)


对于像 JavaScript 和 Ruby 这样的动态语言来说,这不是个小问题。过去 15 年来我一直在学习该如何应对这种错误问题。我在之前谈论的是 2011 年代的情况,其中讨论的缓解措施确实有些用途,但它们无法随着系统的发展顺利地扩展下去,而且我们忘掉它们时也没有安全网可用。


我觉得 15 年时间已经够长了。我想回到静态类型系统的怀抱,毕竟这种系统中根本不会出现这类错误。彼时我们有几个选项:Elm、Reason、Flow、TypeScript 和 PureScript。(这里举了一部分例子。)最后我决定使用 TypeScript 是因为:


  1. TypeScript 是 JavaScript 的超集,因此移植起来很容易。移植回来也更容易些:只需删除类型定义即可,然后我们又回到了 JavaScript。

  2. TypeScript 编译器使用 TypeScript 编写,并作为已编译的 JavaScript 代码分发,因此我们可以在自己的 Web 应用中运行它。我们的 TypeScript 课程正是这样做的:在浏览器中评估用户的 TypeScript 代码,以避免网络延迟。

  3. 这一条是我们的业务特有的:TypeScript 比其他选项更受欢迎。这意味着更多的人希望从像我们这样的课程中学习 TypeScript。用 TypeScript 编写 Execute Program,让我们可以制作出更好的 TypeScript 课程。


在 2018 年 10 月,我们用了大约两天时间将前端 JavaScript 代码移植到了 TypeScript。下面的图表显示了在移植前和移植后每种语言拥有的代码量。



当时我们还是 pre-beta 版本,所以系统还很小,只有大约 6,000 行。这张图上没有涉及移植后的情况;我们将在以后的文章中具体介绍相关内容。


在这次移植之后,React prop 问题消失了。下面我们会看几个示例,首先是一个简单的例子。以下是渲染“Continue”按钮的代码,这个按钮出现在我们课程的每个文本段落之后:


<Button  autofocus={true}  icon="arrowRight"  onClick={continue}  primary>  Continue</Button>
复制代码


这个 Button 组件的 props 的类型如下所示。当读取诸如 autofocus?: boolean 之类的属性类型时:“autofocus”是属性的名称;“?”表示它是可选的;“:”将属性名称与其类型分开;而“boolean”是类型。最后一个属性类型 onClick 表示“一个不带参数且不返回任何内容的函数”。如果你不熟悉 TypeScript 的函数类型语法,可以在我们的课程中全面了解 TypeScript 的函数类型。


type ButtonProps = {  autofocus?: boolean  icon?: IconName  primary?: boolean  onClick: () => void}
复制代码


如果将“autofocus”prop 从 true 更改为 1,会发生什么?现在,我们在类型系统期望一个布尔值的地方传递了一个数字值。不到一秒钟后,编译器将在下面显示错误。(这里删除了一些不相关的细节;本系列文章中所有涉及到错误的地方都会这样处理。)


src/client/components/explanation.tsx(13,27):  error: Type 'number' is not assignable to type 'boolean | undefined'.
复制代码


有害代码在 vim 中也变成了红色。修好它后,红色消失了。解决错误只需要几秒钟。在 Ruby 或 JavaScript 中,我可能会花几分钟的时间手动测试应用程序,并反复浏览它的状态才能知道到底发生了什么事情。我也可以依靠自动化测试,但是我们在另一篇文章中介绍了测试 vs 类型的问题。


这个整数到布尔的更改是对类型系统的一次简单而低风险的测试。Button 的 icon 属性显示了更高级的用法。下面还是 Button 调用:


<Button  autofocus={true}  icon="arrowRight"  onClick={continue}  primary>  Continue</Button>
复制代码


看起来 icon prop 只是一个字符串:“arrowRight”。在运行时,在已编译的 JavaScript 代码中,它将是一个字符串。但是在上面显示的 ButtonProps 类型中,我们将其定义为 IconName,后者是在其他地方定义的。在查看其定义之前,让我们先看看这个类型的作用。假设我们将“icon”prop 更改为“banana”。我们实际上没有名为“banana”的图标。


<Button  autofocus={true}  icon="banana"  onClick={continue}  primary>  Continue</Button>
复制代码


不到一秒钟后,TypeScript 编译器拒绝了这一更改:


src/client/components/explanation.tsx(13,44):  error: Type '"banana"' is not assignable to type    '"menu" | "arrowDown" | "arrowLeft" | ... 21 more ... | undefined'.
复制代码


编译器说“icon”不能是任意字符串。它必须是我们定义为 icon 名称的 24 个字符串之一。编译器将拒绝任何使我们引用不存在图标的更改;这不是有效的程序,甚至无法开始执行。


有多种方法可以实现 IconName 类型。一种是编写一种类型,该类型显式列出所有可能的 icon 名称。然后,我们必须使 icon 名称与其在磁盘上的图像文件保持同步。这种类型可能是这样的:


type IconName =  "menu" |  "arrowDown" |  "arrowLeft" |  "arrowRight" |  ...
复制代码


翻译成中文:“这里会静态地保证 IconName 类型的一个值是此处指定的字符串之一,但不能是其他任何字符串。”(这个类型是我们两堂课程涵盖的两个主题的组合:字面量类型和类型联合)


我们的 IconName 未被定义为字面量类型的简单联合。让图标名称列表与文件列表保持同步是很无聊的工作,我们可以让计算机来完成它!相反,我们的 icon.tsx 文件如下所示:


export const icons = {  arrowDown: {    label: "Down Arrow",    data() {      return <path ... />    }  },  arrowLeft: {    label: "Left Arrow",    data() {      return <path ... />    }  },  ...}
复制代码


实际的 SVG < path/> 标签就在源代码中,在以 icon 名称为键的对象中。(也可以在不将 SVG 内联到源文件中的情况下执行此操作。例如,我们可以使用一些 Webpack 技巧将图像保存在它们自己的文件中,但仍然可以确保列表中的每个图标也都存在于磁盘上。到目前为止,这种简单的解决方案对我们来说是很好用的。)


通过这种方式定义 icon 后,我们可以使用一行代码自动提取其名称的联合类型(union type):


export type IconName = keyof typeof icons
复制代码


(这里的意思是,你可以认为该类型表示“每当某物的类型为”IconName”时,它必须是与 icons 对象的键之一匹配的字符串。)


这样就搞定了;并不需要其他类型层面的工作。剩下的代码只是一个简单的 Icon React 组件,它在列表中查找图标并返回其 SVG 路径。这个函数中没有明确的 TypeScript 类型。它看起来像是纯粹的 JavaScript 代码,但它也经过了类型检查。这是一个最小版本,其中删除了所有无关的细节:


export function Icon(props: {  name: IconName}) {  return <svg>    {icons[props.name].data()}  </svg>}
复制代码


现在,我们可以将 SVG 标签放入这个源文件中,并将新 icon 拖放到“icons”列表中。当我们这样做时,这个 icon 就可以在 Button 组件,以及系统内接受 icon 名称的其他任何部分中使用。如果我们从列表中删除一个 icon,则系统中引用该 icon 的所有部分都将立即无法编译,从而确保没有过时的 icon 引用在运行时导致错误。


这些示例按照静态类型标准来说是很简单的,但我认为它们证明了 Web 应用程序中有多少可以轻松实现的改进之处。一个应用程序中的大多数代码都不涉及高级类型系统功能;多数需求仅仅是“确保我们传递正确的 props”和“确保我们的图标确实存在”之类的简单事情。


我们在整个系统中都做了这种事情。其他的一些示例:


  1. 我们在整个系统中使用了一个 Note 组件。它具有一个 tone prop 来确定提示的样式:“info”“warning”“error”等。如果我们不再使用其中某个 tone 选项,则我们将从联合类型中将其删除,并且所有引用这个 tone 的 Note 将出错,直到我们更新它们。

  2. 我们链接到的每个 URL 都将静态保证存在。当我们重命名或删除 URL 时,链接到它的每个组件都无法编译,直到我们对其进行更新以匹配为止。

  3. 当我们链接到这些 URL 时,类型系统可确保我们填充 URL 中的所有空缺。例如,路径“/courses/:courseId/lessons/:lessonId”具有两个 hole,“courseId”和“lessonId”。如果我们尝试链接到该路径,但忘记提供“courseId”,则代码将无法编译。

  4. 我们在客户端上发出的每个 API 请求都会被静态确保与相应服务端 API 端点的负载结构匹配。如果我们在端点中重命名一个属性,哪怕属性在嵌套的 API 对象的内部深处,引用该端点属性的任何代码也都将无法编译,直到我们对其更新以匹配为止。我们在另一篇文章中介绍了细节。


诸如此类的问题经常会出现在编程工作中,尤其是在动态语言中非常常见;但我们无需编写任何自动测试,也用不着什么手动测试,就可以从静态上避免这些问题。有些问题解决起来需要费些功夫。我们的 API 路由器验证写起来很麻烦。但是写多了就顺手了。上面的单行“IconName”类型实际上是问题的完整解决方案。如果将其复制到 TypeScript 文件中,它就能起作用。


将我们的前端代码移植到 TypeScript 仅仅是个开始。那之后,我们又将后端从 Ruby 移植到了 TypeScript,然后在移植后的 9 个月内对其进行了扩展和维护。


英文原文


Porting a React Frontend to TypeScript


2020 年 6 月 01 日 10:474116
用户头像
小智 InfoQ 主编

发布了 395 篇内容, 共 306.8 次阅读, 收获喜欢 1709 次。

关注

评论

发布
暂无评论
发现更多内容

区块链钱包APP开发服务商,数字货币钱包开发价格

135深圳3055源中瑞8032

字节跳动HR:3年从4000人招到10万人,我经历了什么

Java架构师迁哥

架构师训练营 -week07-作业

大刘

极客大学架构师训练营

USDT承兑支付系统开发技术,搭建商户USDT支付系统

135深圳3055源中瑞8032

【得物技术】数据分析 - 生活品类社区内容精选池模型

得物技术

数据分析 得物技术部 得物技术 社区内容 精选池模型

合约跟单模式开发,合约跟单系统平台

135深圳3055源中瑞8032

这份阿里P8整理的新版手抄本,简直把所有Java知识操作都写出来了

Crud的程序员

Java 编程 程序员 程序人生 java面试

Redis最常见的16道面试题与详解

Java架构师迁哥

隐私计算S2赛季 谁是真正的王者?

hellompc

学习 隐私计算

MySQL中特别实用的几种SQL语句送给大家

陈哈哈

SQL优化 实用SQl语句 高性能SQL

“软件教父”花费20年,教你如何在应用层混迹的风生水起

小Q

Java 学习 架构 面试 应用

LeetCode题解:231. 2的幂,递归,JavaScript,详细注释

Lee Chen

算法 LeetCode 前端进阶训练营

谈谈敏捷开发概念和迭代开发方案

Philips

敏捷开发 快速开发

LeetCode题解:231. 2的幂,迭代,JavaScript,详细注释

Lee Chen

算法 LeetCode 前端进阶训练营

这可能是关于编程指南的最实用指南了

华为云开发者社区

开发者 软件开发 语言

力扣解题:第三题(个人思路整理)

人语驿边桥

力扣

架構師訓練營第 1 期 - 第 07 周作業

Panda

架構師訓練營第 1 期

从技术到应用实践 揭秘京东区块链布局全景

京东智联云开发者

区块链 区块链方案 供应链

HTTP2协议及websocket协议总结

江龙

TCP梳理总结

江龙

干货 | 京东技术中台的Flutter实践之路

京东智联云开发者

flutter

快快使用ModelArts,零基础小白也能玩转AI!

华为云开发者社区

人工智能 开发者 开发

我去!三面字节竟全败在Redis上,带薪摸鱼刷1949页进阶笔记

996小迁

Java redis 架构 面试 程序人生

训练营第三周总结

大脸猫

极客大学架构师训练营

字节跳动大神亲自总结SpringBoot手册,让你可以在简历上写精通SpringBoot!

Java架构追梦

Java 架构 面试 微服务 springboot

啥是数据库范式

Simon

MySQL 数据库 数据库设计

华为发布5GtoB核心网建设白皮书

华为云开发者社区

5G 边缘技术

NPC Follow

katichar

数字资产交易平台开发,场外交易所开发搭建

135深圳3055源中瑞8032

《高效程序员的45个习惯:敏捷开发修炼之道》.pdf

田维常

电子书

低代码开发不靠谱?看低代码开发在物联网APP开发中的应用

华为云开发者社区

技术 软件开发 代码

从React迁移到TypeScript:忍受了15年的JavaScript错误从此走远-InfoQ