likes
comments
collection
share

灵魂发问:你真的在业务项目中用好TypeScript了吗

作者站长头像
站长
· 阅读数 16

目前前端项目中 TypeScript 已经成为主流,业界内很多第三方库也为我们做了很好的示范,在社区中也存在着各式各样的教程,当然还有大神热衷的ts体操非常之烧脑。不过,如果添加一个定语「业务代码」,似乎这样的文章和开源项目数量就稀少很多了。所以,这次想根据自己在真实业务场景下的实践,来聊聊 ts 在业务代码中的使用。当然,我的方案也并非最佳实践,只是代表个人的一种编码风格(求生欲拉满。

1. Why TypeScript

是否使用 TypeScript 在社区和知乎上都有很多声音在讨论类似的问题。反方😡的观点是:使用 TypeScript 最大的问题在于其不低的学习成本和编码过程中写类型的时间成本,如果评价它的 ROI,那我们要从收益的角度来评估这件事,即:当一名前端开发使用 ts 后会发生什么?

  1. 其他的同学如果调用方法时,类型/返回值错误会有静态提示。特别是一些具有复杂 if-else 逻辑的函数,使用 ts 可以清晰地涵盖所有类型返回,帮助我们注意到需求针对某种场景下可能存在的 undefinednull 进行特殊处理。
  2. 获取其他属性的时候有快捷提示。我们经常会调用一个对象中的某个属性或某个方法,在没有提示的情况下需要完整的敲出整个单词,使用 ts 后,1-2个字母就可以帮我们快速定位到目标。
  3. 新接手项目的同学可以通过 typeinterfacereturnType 等类型快速理解每段代码的含义。 大家知道很多同学非常讨厌写注释,那么 ts 的类型在这种情况下就是最好的注释。

综合来看,随着项目体积和人力的增加,使用 ts 的收益也会不断变大🚀。从企业级场景来看,项目的规模一般是比较大的,特别是长期维护的终端项目和后台项目;其次可能因为团队和组织架构的变动,一个项目在它的生命周期中可能被很多同学维护过,为了自己不挨骂,也为了能提高整体的迭代效率,使用 TypeScript 都是非常必要的。

可能有的同学会问:如果是那种使用一次就丢掉的活动页难道还有必要的使用 ts 吗?当然可以不用,不过话说回来,活动页使用一套搭建的、低代码解决方案不是更好维护嘛🐴

2. 利用好类型推断

ts 非常有趣的一点就是类型推断,这对开发者而言是一把“双刃剑”,如果开发者在函数开发/调用的某处声明了 any 类型,那么之后依赖该处的所有地方都会变成 any 类型,会让 ts 类型的价值下降很多徒增工作量。不过,利用好这个能力,反而可以减少一定工作量。 比如我使用 js 编写了如下的函数:

function Test(num) {
  function fnc() {
    return num+num;
  }
  const result = fnc();
  return result;
}

如果为每一处添加添加类型,会变成👇🏻下面这个鸭子:

function Test(num: number): number {
  function fnc(): number {
    return num+num;
  }
  const result: number = fnc();
  return result;
}

通过代码逻辑可以发现,fnc 的返回类型依赖了 num 的类型,result 又依赖了 fnc 的返回类型,函数本身的返回结果又是 result 的类型。所以在本例中,其中只需要声明参数中 num 的类型,其他的类型就可以自动推断出来,不需要手动声明了。

这个例子可能听起来有些离谱,但在真实场景中很多人都会犯类似的错误——有时为了开发方便,先忽略的类型系统,后面又回头补充类型的时候,很大概率会重复声明。不如从编码开始就一点点把类型写好,依靠类型推断的特性,后面也会写起来更加顺畅。

3. No Magic Number

不要使用 Magic Number!

什么是 magic number?指的是代码中出现的没有说明的数字。代码中突然出现一个没说明用途的数字会让其它阅读代码、维护代码的的人非常难受。

比如一个场景要限制最大选中数量,单纯写10、20在逻辑中有时会让人意义不明,改用:

const MAX_ITEM_NUMBER = 10;

通过常量来声明,就会相对清晰很多。 你会问这种规则和 ts 有什么关系?其实在真正的业务场景中,往往多个数字之间存在着一定的联系,那么使用 ts 中的枚举——enum 就会是非常好的方法。

比如在我们的活动后台中的每一项活动都存在着:编辑中、待上线、上线和下线四种状态,那么我们就可以使用枚举来声明:

const enum ActivityStatus {
  Edit,
  Preonline,
  Online,
  Offline
}

那么在渲染的时候就可以通过 Map 映射到枚举很清晰的展现出来:

const statusMap: Readonly<Record<ActivityStatus, string>> = {
    [ActivityStatus.Edit]: '编辑中',
    [ActivityStatus.Preonline]: '待上线',
    [ActivityStatus.Online]: '上线',
    [ActivityStatus.Offline]: '下线',
};

const renderStatus = (item: ActivityStatus) => {
    const desc = statusMap[item] || '';
    return (
      <div>
        {desc}
      </div>
    );
}

当然,针对列表渲染等场景具体的枚举值需要和服务端同学进行协商(通常是服务端定好,因为在服务端状态的变化相对会更加复杂),这其实也帮助到前端同学会更加关注到业务本身的设计。

4. enum 和 const enum

他们俩有什么区别呢,在 ts playground 中把它们编译一下得到的结果就非常清晰了:

灵魂发问:你真的在业务项目中用好TypeScript了吗

使用 enum 声明会得到一个嵌套的对象,即 ActivityStatus2[0]ActivityStatus2["Edit"] 都能互相访问到对象,是一个实际存在的 object;使用 const enum 得到的产物中并不会存在这样的一个对象,只会转变为原来的 magic number。

到底使用哪一个好见仁见智,最重要的是项目中风格保持统一即可。从我个人的观点出发,当项目中大量了使用枚举之后,使用普通 enum 会生成大量的对象增大主 bundle 的体积,从性能的角度产物整洁的角度来看,const enum 会是我的首选。

5. 约束 string literal

在很多业务场景下,我们封装的某个方法往往不是对所有 string 类型的参数都生效的,这个时候我们最好的方法就是通过 type 来限制字符串的类型。

比如我在业务代码中封装了项目页面携带部分参数跳转的方法,那就可以这样做:

type JumpPathType = 'center' | 'about' | 'home';

function gotoPage(path: JumpPathType){
    // do something
}

相比较于普通的 sting 有以下几点好处:

  1. ✅ 代替注释。与其注释“该函数仅针对本项目路由生效”不如通过这样的方式来减少没必要的注释,更加明了。
  2. ✅ 拼写纠正。比如 centercentre 就是经常会弄混的两个单词,ts 的静态检查可以第一时间发现单词拼写问题(我的真实案例,当时各种排查函数实现最后才发现是单词拼错了)。
  3. ✅ 方便协作与迭代。假如你的某位同事不小心修改了这个函数移除了对 home 路由的支持且并未告知其他人,当你 pull 下来它的代码后,对应调用的地方就会飘红,并且启动项目时会有命令行的提示,可以第一时间发现变动。

除此之外,ts 中的模板字符串类型也十分好用。

比如我们的某个字符串类型字段的规则形如“ID1.ID2”,那么可以这样约束它的类型:

type SourceCodeType = `${number}.${number}`;

在后端序列化返回字符串 "true""false" 的时候,也可以使用:

type isLiveType = `${boolean}`;

所以使用好 ts 中的模板字符串可以帮助我们在更具体的业务场景中做静态的类型检查,相比较与普通的 string 类型,功能会有很大的提升。

6. 封装请求泛型

在发起请求时,我们关注请求的参数和接收服务端的返回值,那么就可以用泛型来约束入参的结果,这可以很好的帮助到我们:

  1. 审查传入的参数是否完整,类型是否正确
  2. 快速拿到返回值,特别是在列表渲染的场景,能快速获取到列表项的属性。

可以通过封装一个通用函数来实现该能力:

interface BasicRequest<T> {
  url: string;
  data?: T;
  /** 其他参数略过 */
  [key: string]:any;
}

interface PlainObject {
  [key: string]:any;
}

export const fetchData = async <Request = PlainObject, Response = PlainObject>(
    params: BasicRequest<Request>
) => {
    const res = await request(getParams(params));
    
    if (isSuccess(res)) {
      const data: Request = res.data;
      return res.data;
    } else {
       return getErrorType(res);
    }
}

那么我们在业务代码中就可以这么写:

interface DetailListItem {
  name?: string;
  age?: number;
  desc?: string;
}

interface FetcheDetailRequest {
  itemId: string;
}

interface FetchDetailResponse {
  data: DetailListItem[];
}

const res = fetchData<FetcheDetailRequest, FetchDetailResponse>({
  url: 'api.test.detail',
  data: {
    itemId: '123456'
  }
});

当我们拿到返回结果后,就可以很轻松地调用其中的属性。

灵魂发问:你真的在业务项目中用好TypeScript了吗

到这里有的朋友一定会说,在真实业务场景中,服务端返回的往往是非常庞大的一个对象,我难道要一行行“费时费力”地把它们转换为 ts 中的 interface 吗?

当然不用,在开源社区有非常多的工具帮助我们做转换,如果你对接的服务端使用 golang,那么有通过 protobuf 转换成 ts 类型的工具;如果他使用的是 swagger-ui 生成的接口文档,也有可以解析生成 ts 类型的工具;即使服务端同学很朴素只告诉你了对应的 mock 数据,也可以使用 json2ts 工具来生成,我个人也写过一个转换小工具来做这件事情。可以看到效果还不错,能节省不少时间。

灵魂发问:你真的在业务项目中用好TypeScript了吗

7. 使用类型的小技巧

最后讲几个编码中可以提升大家编写 ts 类型速度的小技巧。

(1)依赖第三方库

一般的第三方库都会将使用到的类型在入口文件一并导出,所以我们跳到源码看一下是否有需要的类型。

比如像内联的 CSS 属性,在某个组件中有一段用到了 style?.width 的逻辑,但是我们在默认情况下将它指定为了空对象,但是空对象中并不存在 width 属性就会报错,这时就可以直接引用框架提供的类型:

import type { CSSProperties } from 'rax';

{
  style = {} as CSSProperties,
}

(2)通过 typeof 获取类型

对于下面这段数据:

const info = {
  type: VersionStatus.Invalid,
  title: '快来更新手机淘宝',
  subTitle: '你的版本太低,暂不支持互动~立刻升级参与活动吧!',
  okText: '马上更新',
};

可以通过 typeof 关键字快速分析出它的类型,当我们已知一段常量想获取它的类型,这一招非常好用。

灵魂发问:你真的在业务项目中用好TypeScript了吗

(3)快速添加&删除属性

interface Student {
  name: string;
  age: number;
}

type Student2 = Pick<Student, 'name'>

type Student3 = Omit<Student, 'name'>

type Student4 = Student & { gender: 0 | 1 }

interface Student5 extends Student {
  gender: 0 | 1
}

由上到下分别是:

  • 仅保留 name;
  • 删除 name;
  • 添加 gender;
  • 添加 gender。

那么在什么样的场景下会使用到这个能力,举个例子🌰:比如你的某个函数传入的参数为类型A,现在你想对这个参数增加一个 isValid 字段并做相应逻辑处理,但这个类型A又被其他人所依赖,他们的逻辑不需要增加新的字段,那么就可以在你的函数入口中单独扩展字段处理。

(4)使用 unknown 代替 any

在一开始不确定类型的有很多同学会选择使用 any 来声明,这会造成非常大的一个问题,那就是后续和该变量相关的类型推断都会直接失效,让 ts 整体的作用大幅削减。

正确的做法:针对那些不确定的变量先声明为 unknown,这样在后面调用时依然会对该变量进行类型检查,后续再利用 ts 中「类型收窄」的能力,推断出该变量真实的类型,具体示例如下:

灵魂发问:你真的在业务项目中用好TypeScript了吗

8. 结语

  1. 通过上面👆🏻的例子我们可以看得出,即使在业务逻辑中如果可以完整地使用 ts 类型会让代码整体的质量和可读性提升很多。
  2. 写这篇文章的同时其实也是又一次审视自己的代码,重新思考🤔这样设计是否合理,怎样才能避免去写 anyScript,当然对 ts 的掌握本身就是在不断的摸索和实践的过程中提升熟练度。之前有读过《clean code》,优秀的代码和编码风格就是在一次次 review 后重新思考设计才更上一层楼的。
  3. 比起工具、组件、前端架构等方向的研发,业务才是当下前端最主要的工作,如何平衡好「代码质量」和「敏捷开发」这两点确实是一件很有趣、很值得探索的事情,如果你觉得文章中哪里有问题,请及时指出勘误,如果你还有什么不错的实践也欢迎大家一起探讨~