前端研发提质增效利器,TypeScirpt成功迁移详解

news2025/1/24 2:26:39

点击蓝字👆 关注Agilean,获取一手干货

直播预告:Adapt 系列直播又双叒叕来啦!最新一期我们将围绕「版本分支与环境」进行深入探讨,欢迎大家来直播间和主播互动哟~

点击下方右上角红色按钮「预约」👇


以下为正文

背景

由于在页面交互行为等层面的优越性,许多研发部门的前端团队会采用JavaScript这一门编程语言。然而,JavaScript是一种动态类型语言,这意味着数据类型错误只会在运行时被发现。运行时类型检查本身并不是缺点:它提供了更大的灵活性,但是项目和团队越大,代码的数量和复杂度越来越高,整个项目的可维护性、鲁棒性(稳健性)会越来越差,进而影响到整个团队的开发效率和质量。

因此,越来越多的前端团队开始考虑替换方案,迁移到TypeScript便是一个出路。

TypeScript是一个由 Microsoft 开发的开源编程语言,它是 JavaScript 的超集,提供了强类型和其他高级功能,能够显著提高代码可读性和可维护性。借助 TypeScript ,开发人员可以更轻松地识别和预防潜在的类型错误,从而减少代码中的bug数量,并更快速地进行开发和重构。

本文,我们将分享知微前端团队的 TypeScript 迁移经验,细数迁移的前因后果与实施注意点。希望能给正在(或者准备)迁移的伙伴们提供一些实实在在的帮助。

成也 JavaScript ,败也 JavaScript 

知微前端团队最初是基于 JavaScript 进行开发的。

初期,JavaScript灵活性可以帮助我们快速构建项目,但是随着项目的复杂度的不断增加,这种模式下暴露出来的问题也越来越多,集中体现在以下几个方面:

开发效率低,线上 bug 多

JavaScript 不会对变量类型进行检查,意味着当存在问题时就无法像其他静态类型语言一样在编译期就抛出异常,只能在运行时才能被发现,这不仅需要开发人员在自测阶段耗费大量的时间,还容易让 bug 逃逸到测试阶段甚至是线上。

872c1ba347ec927415abd2242b01c548.png

可维护性差

JavaScript 变量的类型可以随时更改,这使得代码难以预测和理解;全局变量可能会被其他部分的代码意外修改,从而导致意料之外的影响,这种情况很难被发现和调试,对代码的可维护性造成了很大压力。

f810fce37d0c90cf4423b90220905e28.png

旧代码不敢碰

每次不得不修改到旧代码,都只能依靠人力进行验证,但人的精力是有极限的。

07fe906964c5c3db176a9ae28fd1743f.gif

IDE 自动补全差

由于 JavaScript 是一门动态语言,其类型在运行时才被确定,因此,在开发过程中,IDE难以准确地确定变量和函数的类型和作用域,这导致代码自动补全的准确性随之下降,甚至无法自动补全。

后浪推前浪,TypeScript “入主”

彼时正好 TypeScirpt 在社区比较火,它作为 JavaScript 的一个超集,扩展了 JavaScript 的语法,增加了静态类型检查、接口、泛型等特性,并且可以被编译成普通的 JavaScript 代码。

这几乎完美契合我们的需求:静态类型检查和兼容 JavaScript ,因此我们决定迁移到 TypeScript 。

1. 步步为营

迁移进行时

在迁移 JavaScript 代码到 TypeScript 时,我们可以采用不同的策略,具体取决于团队的特定需求和现有代码的质量。以下是一些常见的迁移策略:

渐进式迁移

这是最常见的迁移策略之一,会涉及 TypeScript 代码与 JavaScript 代码一起使用的过渡期,以便逐步迁移到 TypeScript 。

在这种策略下,我们可以通过在新的 TypeScript 模块中编写新代码,逐步替换旧代码,并使用 TypeScript 的工具和插件来检查和改进代码,实现渐进式迁移。

重构策略

如果代码规模比较小,或者我们需要在 TypeScript 中完全重写代码,那么整体重构可能是一个更好的选择。

在这种情况下,我们需要重新审查我们的代码并将其重写为 TypeScript ,这可能需要更多的时间和精力。但是,如果能成功地重构代码,我们将能够更好地利用 TypeScript 的优势。

基于成本考虑,我们采用了渐进式迁移的策略来将 JavaScript 代码迁移到 TypeScript 。

这种策略的好处在于,它可以让我们在不破坏现有代码的情况下,逐步替换旧代码,并确保新的 TypeScript 代码符合 TypeScript 的语法规则和最佳实践。

具体来说,我们经历了以下几个阶段:

1

配置支持 TypeScript 和 JavaScript 共存

因为 TypeScript 可以通过 tsconfig.json 配置允许 JavaScript 和检查的严格程度,我们可以在项目保持旧 JavaScript 代码的同时引入 TypeScript ,并将 TypeScript 检查的严格程度调整到最低,以免项目无法编译通过,这样可以使得迁移过程更加平滑和稳定,并且可以在迁移过程中避免一些不必要的问题。

2

新代码要求使用 TypeScript 编写

我们要求团队成员新的代码一律使用 TypeScript 编写,但由于当前的 TypeScript 类型检查程度开的是最低的,所以无法通过 tsc 这样的静态类型检测来保证全员遵守 TypeScript 最佳实践,需要额外的办法来确保该机制的运行,比如Code Review,避免将 TypeScript 用成 AnyScript 。

3

旧代码的迁移

在需求的迭代过程中将涉及到的 JavaScript 逐步迁移到 TypeScript ,或者是在版本的迭代过程中穿插一些技术改造需求。

随着新代码的增加和旧代码的迁移,TypeScript 覆盖率到达一定程度后,可以将 TypeScript 的编译器配置逐步调整进行更严格的类型约束,最终实现 TypeScript 覆盖率达到100%。

2.金无足赤

迁移后的利与弊

由于我们以前都是使用 JavaScript 开发,刚刚引入 TypeScript 后有不少需要适应的地方:

弊端

  • 需求开发所需要的时间增加了。

    因为 TypeScript 是一种静态类型语言,开发人员需要花费额外的精力来定义变量和函数的类型,并确保代码的正确性和可用性。这可能会导致开发过程中的一些延迟和额外的努力。

  • 对于没有静态类型编程经验的开发人员来说,使用 TypeScript 可能会有些不习惯。

    这是因为 TypeScript 要求我们对代码中的变量和函数进行类型定义,并在编写代码时就要考虑类型的一致性和正确性。这可能需要一些额外的学习和适应时间。

  • 对于没有提供 TypeScript 声明的第三方库,需要自己编写类型声明文件。

  • 因为浏览器仅支持 JavaScript,需要将 TypeScript 转译为 JavaScript ,所以打包相比以前多了一步将 TypeScript 转译为 JavaScript ,具体耗时取决于项目的大小,小型项目可能编译变慢的问题不明显,但中大型项目则影响会比较大。

  • 电脑需求面临更高的性能挑战。

    比如 VSCode 需要开启 ts-server 实时运算校验类型和提示,消耗资源过多可能会导致 VSCode 变卡,进而降低开发效率(有理由换性能更强大的电脑了)。

尽管存在以上问题,但整体而言 TypeScript 仍然是一个值得投资的选择:

优势

  • 相较 JavaScript,更强的 IDE 提示和自动补全功能。比如前端开发看后端某个接口返回值,一般需要去 Network 或接口文档看才能知道返回数据结构,而正确用了 TypeScript 后,IDE 会提醒接口返回值的类型,这节省了不少时间。

  • 支持静态类型检测。使用 TypeScript 我们可以使用其提供的 tsc 命令进行整个项目的类型检测,直接检测出当前项目存在哪些类型错误,而不需要在运行时去人工检测代码是否会引起异常以及担心是否有遗漏的适配,大大减少了开发人员的心智负担和自测耗时,同时,还降低了 bug 逃逸率,减少了线上 bug,简直是农耕时代和工业时代的区别。

  • 及早发现问题。借助 IDE 对 TypeScript 的支持,在编码过程中就能够发现部分问题,可以极大提升开发效率(项目规模越大,差别会愈加显著)。

  • 代码可读性与可维护性更佳。使用 TypeScript 可以帮助我们更好地组织和结构化代码,并提供更明确的类型定义和命名约定。这使得代码更易于阅读和理解,也更容易维护。

  • 代码重构能力更强。使用 TypeScript 可以帮助我们更快速地进行代码重构,因为我们可以在编译时就捕获到许多潜在的问题。这使得代码重构更加安全、可靠,也更容易进行。

3.披巾斩棘

使用 TypeScript 遇到的问题及解决方法

对于一些第三方库无法进行较好的类型约束

比如,在 redux-saga 中,我们无法直接对 action 和对应 saga 进行类型约束关联。

TypeScript
// keys.ts
export const fetchUserKey = 'FETCH_USER_ACTION';

TypeScript
// actions.ts
import { fetchUserKey } from './keys'

function fetchUserAction(userId: string) {
   return { type: fetchUserKey, payload: { userId } };
}

TypeScript
// sagas.ts
import { takeEvery } from 'redux-saga/effects';
import { fetchUserKey } from './keys'

function* fetchUserSaga(action: { type: string; payload: { userId: string } }) {
   const { type, payload } = action;
}

takeEvery(fetchUserKey, fetchUserSaga);

在上述示例代码中,fetchUserAction 和 fetchUserSaga 这两个类型理论上是有联系的,该 Action 的返回值即为Saga的入参,虽然我们使用了 TypeScript 定义了fetchUserAction 和 fetchUserSaga 的参数类型,但这两个类型间的联系是割裂的,只要两者中任一处的类型被调整了,另一处都无法通过 TypeScript 静态类型检查得到反馈。

针对这类场景,我们可以借助 TypeScript 的类型编程来实现手动绑定类型:

TypeScript
// types.ts
import type { Action } from 'redux';

/** 用来从 actionCreator 中推断出 payload 的类型 */
export type ActionPayload<T extends (...args: any[]) => { payload: unknown }> = ReturnType<T>['payload'];

/** 如果你的 action creator 全部放在一个 record 中的话,你可以使用该类型推断生成 Payload record */
export type ActionPayloadRecord<T extends { [key: string]: (...args: any[]) => { payload: unknown } }> = {
   [K in keyof T]: ActionPayload<T[K]>;
};

export interface AGAction<T> extends Action<string> {
  type: string;
   payload?: T | null;
}

TypeScript
// payload.ts
import type { ActionPayloadRecord } from './types';
import type * as actions from './actions';

export type Payload = ActionPayloadRecord<typeof actions>;

TypeScript
// sagas.ts
import { takeEvery } from 'redux-saga/effects';
import { fetchUserKey } from './keys'
import type { Payload } from './payload.ts'
import type { AGAction } from './types.ts'

// 这里的 AGAction<Payload['fetchUserAction']> 就实现了 fetchUserAction 的返回值与 fetchUserSaga 入参的类型绑定
function* fetchUserSaga(action: AGAction<Payload['fetchUserAction']>) {
   const { type, payload } = action;
}

takeEvery(fetchUserKey, fetchUserSaga);

通过上述代码,我们可以实现当 fetchUserAction 的返回值类型发生变更时,能够通过 TypeScript 的静态类型检测得到类型错误提示。而且上述其实大部分都是模板代码,因此我们可以通过编写一些工具通过入参来自动生成这些模板代码。

TypeScript 还在不断完善

尽管 TypeScript 在过去几年中已经获得了广泛的认可和采用,但它仍是一个相对年轻的语言,因此仍然存在一些特性的支持不够完善的情况。

例如,在 TypeScript 4.1 之前,如果从一个 Record 类型的对象中获取一个属性值,那么这个属性值的类型将被假定不会为 null 或 undefined 。这意味着如果你尝试获取一个不存在的属性或者一个属性值可能为空的情况,TypeScript 将不会在编译时给出任何警告,这可能导致在运行时出现错误。

TypeScript
const record: Record<string, string> = { 'a': '1', 'b': '2'  };
// TS 4.1 之前或未开启 noUncheckedIndexedAccess 选项时,此处 value 的类型为 string,而非实际的 string | undefined
const value = record['11'];

为此,我们通过TypeScript自定义出一个KVMap类型:

TypeScript
export type KVMap<Key extends keyof any, Value> = { [index in Key]?: Value };

借助 KVMap 我们实现了安全地进行索引取值,但是KVMap这个类型仍有局限性,在一些内置方法,比如 Object.entires、Object.values 等方法中,其返回值的类型又多了冗余的 undefined,整体还不是很优雅(小编吐槽:你们啥时候优雅过了🙄️)。

直到 TypeScript 4.1 引入了 noUncheckedIndexedAccess选项。通过启用这个选项,开发者可以告诉TypeScript认为从索引中获取的值具有可能为空的类型,完美地解决了Record 和 KVMap 的局限性。

虽然 TypeScript 仍然处于成长阶段,并且可能存在某些特性的支持不够完善的情况,但它的发展速度非常迅速, TypeScript 的开发团队正在不断地推出新的版本和更新,以支持更多的语言特性和更好地解决已知的问题。

4. 里应外合

  TypeScript生态内闭环,生态外守护

尽管 TypeScript 提供了静态类型检查,但是在实际开发中,我们经常需要处理从外部接收的数据,例如从API接口、数据库等场景中接收的数据。这些数据的类型可能与我们在代码中定义的类型不一致,因此我们需要进行运行时类型校验,以确保数据的准确性。

基于前后端互不信任原则,以下代码只能是声明接口会返回一个User类型的数据,但无法保证API返回的数据是严格遵守类型声明。

TypeScript
interface User {
   name: string;
   age: number;
}

const fetchUser = (): Promise<User> => {
   return fetch('xxx');
}

如果直接使用以上代码,就有可能让非预期数据进入到前端业务处理代码,因为非法操作而导致前端页面崩溃,更可怕的是经过前端将错误数据又传到后端数据库进行存储,会进一步导致其他关联业务出现错误,一旦出现这种问题就非常难以排查,因为出错的地方并不是根源所在。

基于上述,我们引入了运行时类型校验的库 zod,用来对外部系统的输入进行校验。只有在校验通过后该数据才能进入到前端业务代码内部,这样既避免外部系统的错误输入不会导致前端页面崩溃,又确保前端项目代码内部的类型一定是正确的。

TypeScript
import * as z from 'zod';

const schemeUser = z.object({
 name: z.string(),
   age: z.number()
})

type User = z.infer<typeof schemeUser>

const fetchUser = (): Promise<User | undefined> => {
   return fetch('xxx').then((it) => {
       const ret = schemeUser.parse(it);
       return ret.success ? ret.data : undefined;
   });
}

小结

以上是知微前端团队的 TypeScript 迁移历程。

虽然 TypeScript 相比 JavaScript 有很多好处,但在选择使用 TypeScript 还是 JavaScript 时,我们需要根据具体情况进行权衡,因地制宜。

总的来说,对于小型项目或者对项目要求较低的情况下,JavaScript 是一个不错的选择。因为它具有较高的灵活性和易用性,而且学习成本相对较低。而对于中大型项目, TypeScript 可以提供更好的代码维护性和可读性,提高团队协作的效率。


本文作者|欧阳城

知微前端团队负责人

e5f9463901494a80d91d78d951ab1168.png

直播别忘了约9d6a817b265051f55cf7f3a6506cb7d0.png,因为目前不考虑回放,所以感兴趣的伙伴记得准时来喔!

550fb8a39dfd06655d3b80a69ddcdc9f.jpeg

分享👇 收藏👇                     点赞👇在看👇

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/433922.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ESP32设备驱动-PAJ7620手势传感器驱动

PAJ7620手势传感器驱动 文章目录 PAJ7620手势传感器驱动2、硬件准备3、软件准备4、驱动实现PAJ7620 将手势识别功能与通用 I2C 接口集成到单个芯片中,形成图像分析传感器系统。 可识别上、下、左、右、前、后、顺时针、逆时针、挥手等9种人手手势。 它还提供内置的接近检测,以…

网络编程及项目思路

计算机和计算机之间通过网络进行数据传输 常见的软件架构&#xff1a; C/S:客户端/服务器 画面可以做的非常精美&#xff0c;用户体验好需要开发客户端&#xff0c;也需要开发服务端用户需要下载和更新的时候太麻烦 B/S:浏览器/服务器 不需要开发客户端&#xff0c;只需要…

java IO流_1

目录 分类 字节流 InputStream OutputStream 文件拷贝 字符流 FileReader FileWriter 处理流 BufferedReader BufferedWriter 文本拷贝 流是从起源到接受的有序数据&#xff0c;通过流的方式允许程序使用相同的方式来访问不同的输入/输出源。 分类 按数据…

SDK(动态链接库dll)的封装技巧

SDK(动态链接库dll)的封装技巧 一、说明 通过上篇文章&#xff0c;我们知道对于封装API&#xff0c;目的为了代码复用等&#xff0c;其中还有一个重要的原因&#xff0c;就是隐藏实现。 说到隐藏实现&#xff0c;在封装C的SDK库&#xff08;动态dll库&#xff09;时&#xff…

【获奖案例巡展】信创先锋之星——浙江省某市区视频能力中心

为表彰使用大数据、人工智能等基础软件为企业、行业或世界做出杰出贡献和巨大创新的标杆项目&#xff0c;星环科技自2021年推出了“新科技 星力量” 星环科技科技实践案例评选活动&#xff0c;旨在为各行业提供更多的优秀产品案例&#xff0c;彰显技术改变世界的力量&#xff0…

【id:33】【20分】C. 分数类(类与构造)

题目描述 完成下列分数类的实现: class CFraction { private: int fz, fm; public: CFraction(int fz_val, int fm_val) ; CFraction add(const CFraction &r); CFraction sub(const CFraction &r); CFraction mul(const CFraction &r);…

elementui的table组件,大量使用v-if导致列表渲染错乱,有的列渲染的值不对,有的列渲染出来的空值解决办法

记录一下&#xff1a; 这是同事碰上的&#xff0c;感觉挺奇怪的就研究了研究。 先说一下&#xff0c;之所以是同事那边碰到的而我这边碰不到这个问题&#xff0c;是因为我这边做 el-table-column 的时候&#xff0c;是先定义的 tableHeader: [...] 然后通过 v-for 遍历出来的…

【Linux】CentOS桥接模式配置静态IP

文章目录 1 前言2 桥接模式和NAT模式有什么区别3 校园网环境下配置桥接模式 1 前言 最近在安装的虚拟机上面用mosquitto搭建MQTT服务器&#xff0c;但是很奇怪的是每次电脑上的测试软件能顺利连接服务器&#xff0c;但是连接电脑热点的外部设备却不行&#xff0c;让我很是困惑。…

程序员随时担心被抛弃......大厂外包值不值得去?

外包”这个词经常被人提及&#xff0c;而且也经常被我们所“鄙夷”&#xff0c;很多人都在四处问&#xff1a;“软件外包公司到底能不能去”&#xff1f; 外包公司到底能不能学到真正的技术&#xff1f; 外包大厂能不能去&#xff1f; 今天就给大家详细分享下外包的利与弊 做…

DNS缓存失效,死循环访问造成连接数瞬间飚高的问题

0.背景介绍 某服务domain.com.cn 之前DNS解析到服务真实地址10.1.1.11&#xff0c;后面需要对用户登录增加黑名单功能&#xff0c;于是在openresty针对服务domain.com.cn的特性完成了黑名单功能。黑名单功能已经上线几个月&#xff0c;但是DNS从服务真实地址10.1.1.11切换到ope…

如何远程自己家里电脑

即使您认真地将数据保存到云中&#xff0c;远程访问家庭或办公室计算机上的计算机文件也可能会改变游戏规则。也许您正坐在旅馆房间里&#xff0c;需要在家用电脑上运行一个程序。 或者您出门在外&#xff0c;想在手机上运行桌面应用程序。 这就是远程桌面工具的用武之地。配置…

【ansj分词,英文被转换为了小写】

ansj分词&#xff0c;英文被转换为了小写 没有识别到词表解决方案jieba分词 没有识别到词表 因为数据是大写&#xff0c;词表是大写&#xff0c;ansj将数据转换成了小写&#xff0c;无法匹配&#xff0c;词表失效。将词表改为小写就可以成功分词。 这里ansj我们的词表就是一列…

rdma IBV_SEND_FENCE标记位理解

参考rxe驱动代码&#xff0c; 在rxe_req.c文件里&#xff0c;取wqe时&#xff0c;会调用req_next_wqe函数&#xff0c; 在函数的最后有个判断&#xff0c; qp->req.wait_fence设置为1&#xff0c;然后返回NULL&#xff0c;所以rxe_requester函数直接退出了&#xff0c;不会…

【Linux】网络编程入门(TCP + UDP)

目录简述&#xff1a; 前言&#xff1a; 一、网络通信概述 二、网络编程主要函数 &#xff08;1&#xff09;socket函数 &#xff08;2&#xff09;bind函数 &#xff08;3&#xff09;bind函数 &#xff08;4&#xff09;accept函数 &#xff08;5&#xff09;connect…

AttributeError: ‘LTP‘ object has no attribute ‘seg‘解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

【Jenkins 2.x 实践指南】2.7 pipeline -- 章节小结

目录 一、 pipeline入门 1. pipeline 是什么 2. Jenkinsfile 又是什么 3. pipeline语法 的选择 4. 创建第一个pipeline - helloworld Stage 1: 创建任务 Stage 2: 创建流水线 - Hello World Stage 3: 编写 pipeline 声明 Stage 4: 执行 Stage 5: 点击 #1 查看日志 …

2023年 中国制造业这三大趋势不可忽视

政府要掏1个亿奖励制造企业搞发展&#xff0c;我国制造业大翻身的时代来了吗&#xff1f; 4月12日成都日报电&#xff0c;为支持制造业创新发展&#xff0c;支持制造业数字化、智能化和绿色化转型升级&#xff0c;培育高精尖特企业&#xff0c;政府给扶持政策不说&#xff0c;…

2.5亿行文本数据,导入到SqlServer需要多久?

今天测试了一下SqlServer的导入性能&#xff0c;数据库版本是SqlServer2008r2。 数据源是大概5000个文件&#xff0c;总共12.7G的大小的文本文件&#xff0c; 自己做的导入工具&#xff0c;利用多线程导入&#xff0c;总共耗费了大概1个小时。 导入后的总行数达到2.5亿多行&am…

Linux 安装tomcat教程

前言 PS&#xff1a;安装tomcat需要先配置jdk 1.Tomcat下载 1.1.Tomcat安装包下载 1. 可以选择直接在omcat官网Apache Tomcat - Apache Tomcat 9 Software Downloads下载&#xff0c;上传至linux服务器 1.2. 主机wget源下载 也可以通过直接在linux直接下载 #执行命令 wge…

Spring AOP: 多切面的顺序,性能及异常监控

目录标题 一、Sprig的AOP操作JDK动态代理CGLib动态代理基于xml开发Spring AOP基于注解开发Spring AOP 二、多切面的顺序基于注解的配置基于Ordered接口配置基于XML配置 三、性能及异常监控性能监控异常监控 四、工程目录及运行结果图 一、Sprig的AOP操作 JDK动态代理 //接口 …