将一个JS项目重构成TS后有感

493 阅读4分钟

前言

这两天将一个 vite+js 的项目用 vue-cli+ts 重构了一遍

先说背景,这是学校课程的一个项目,团队基本都是小白,学习带编写,持续了一年

再说项目结构,我习惯 一个文件只默认导出一个函数/数据,在模块的 index 文件中合并导出,所有文件都从模块中间件导入数据

这个项目功能还是蛮复杂的,分了百来个文件,只能一个个修改,也是第一次用 ts 写大项目,积累了一些 ts 的经验,总结分享一下

为什么

为什么选 vue-cli

这个项目开展的时候,当时 vite2 刚出,想用个比较新的工具

而且 vite 的启动速度确实比 vue-cli 快,也就选择了 vite

然而,项目大了之后,vite 的热更新非常慢

也可能是因为我项目结构的问题,js 文件彼此间的引用较为复杂,我在 js 文件中保存一下,热更新时页面会自动刷新,加载 3-5 秒才能看到效果

把项目复制到 vue-cli 中,修改同一处文件,页面不需要刷新,瞬间出结果

至于为什么会这样,我没学过二者的原理,不太了解,就我感受而言,vite 的热更新是真的拉跨,所以选择了 vue-cli

为什么选 ts

本来项目开展时就打算用 ts 的,但照顾到其他零基础的成员,就没用

现在项目验收结束了,就用 ts 重构一遍吧

最近 ts 比较热门,类型推断和参数限定也是很有用的

接下来聊聊重构期间用到的一些较为重要的知识吧

断言

类型断言

ts 中有两种类型断言方式,前置断言 <> 和 后置断言 as

const obj: { a?: number } = {}
Math.abs(<number>obj.a)
Math.abs(obj.a as number)

因为 obj.a 可能为空,直接操作会报错,如果确定调用时已经赋值,可以断言为所需的类型

非空断言

上面哪个例子,也可以通过非空断言来实现

一种方式是用 if 包裹

const obj: { a?: number } = {}
const a = obj.a
if (a) {
  Math.abs(a)
}

也可以后置 ! 来进行非空断言

const obj: { a?: number } = {}
Math.abs(obj.a!)

在赋值时断言

看看下面这个例子

const canvasData: CanvasData = {
  canvas: undefined,
}
interface CanvasData {
  canvas?: HTMLCanvasElement
}
// 每次使用都需要非空断言
canvasData.canvas!.width
canvasData.canvas!.height

// 页面挂载就赋值
onMounted(() => {
  const vm = getCurrentInstance()
  canvasData.canvas = vm!.refs.canvas as HTMLCanvasElement
})

要存储 canvas 元素,因为在设置初值时只能为 undefined,那么接口类型就会包含 undefined,在使用时也需要非空断言,较为麻烦

可以采取下面的方式

const canvasData: CanvasData = {
  canvas: <HTMLCanvasElement>(<any>undefined),
}
interface CanvasData {
  canvas: HTMLCanvasElement
}
// 使用不需要非空断言
canvasData.canvas.width
canvasData.canvas.height

anyunknown 都表示任意类型,也可以用于断言任何类型,通过两次断言,将 undefined 作为 HTMLCanvasElement 类型赋值

函数

获取返回值类型

可以通过 ReturnType 获取一个函数的类型

在项目中有一个函数,其返回值过于复杂,不想专门写接口,就可以使用 ReturnType<typeof fun> 获取该函数的返回值并取别名

// 获取数据,返回类型非常复杂
function getData(num: number) {
  if (num > 1) {
    return {
      sign: true,
      a: 1,
      b: 1,
      ……
    }
  } else {
    return {
      sign: false,
      a: 0,
      bb: 1,
      ……
    }
  }
}

// 获取函数返回类型并取别名
type DataType = ReturnType<typeof getData>

function useData(data: DataType) {
  // 根据标识区分使用哪类数据
  if (data.sign) {
    data.b
  } else {
    data.bb
  }
}

重载

当一个函数根据参数区别返回值时,可以使用重载

下面这个函数,传入 'bar' 则返回值为数字类型,传入 time 则将数字转换为时间字符串再返回

function getBarOrTime(type: 'bar'): number
function getBarOrTime(type: 'time'): string
function getBarOrTime(type: string) {……}

枚举

我们可以用带名字的常量,清晰的表达意图

之前的js版本

// 模式 1-选择 2-添加 3-裁剪 4-删除
const mode = ref(1)

if (mode.value == 1) {……}

用 ts 的枚举

const mode = ref(Mode.Select)

enum Mode {
  Select = 1,
  Add,
  Tailor,
  Delete,
}

if (mode.value == Mode.Select) {……}

在 js 中也可以定义对象来实现和枚举相同的效果,二者的区别在于编码阶段,枚举会被编码成数字,而对象仍会保留

疑问

在重构期间遇到的几个问题,到现在还没有解决

  • *.vue 文件的 <script> 中设置 lang="ts",就会直接变为 tsx 环境,前置断言 <> 将不允许使用

  • vue 不认识 pointer-events,没有被化为 css 属性中,直接赋值给 style 会报错

    我的处理方式是转成 any,不知道有没有其他处理方式

    <div :style="(styleObj as any)"></div>
    
  • 防抖函数的类型怎么写? 我在 2000字助你精通防抖与节流 中实现的防抖函数,在尝试为其写 ts 类型时无法完美书写,参数、返回值、this 总有一个报错,目前只能用 any 解决

对于以上问题,如果掘有解决方案或想法,救救孩子吧

结语

这些就是这次重构时得到的 TS 经验了

如果有所帮助,希望能点赞关注,鼓励一下作者