使用 TypeScript 的 CheckJS 为你的陈旧 JavaScript 项目续命

1,615 阅读3分钟

🙋 Why CheckJS?

  • 让 JavaScript 项目也能享受到 TS 的类型推导等诸多好处。

  • 和直迁 TypeScript 相比,大大降低成本和风险,例如:

🚥 使用方法

安装依赖、追加配置

# 为你的项目安装 TypeScript
npm install typescript

# 可选的:类型定义文件,按照自己的项目需要酌情添加
npm install @types/react -D

根目录新建 tsconfig.json,复制以下内容,特别注意黄色加粗的内容:

{
  "compilerOptions": {
    "outDir": "./FAKE_DIR",
    "target": "ESNext",
    "module": "CommonJS",
    "esModuleInterop": true,
    "allowJs": true,
    "checkJs": true,
    "strictNullChecks": true,
    "jsx": "react"
  },
  "include": [
    // 根据项目目录结构自行配置
    "./src/**/*"
  ],
  "exclude": [
    // 根据项目特性按需添加
  ]
}

编写类型定义

编写类型定义的时候最好有一定的约束,可以防止类型定义冲突、提高代码结构可读性,我个人推荐下面这些方案。

目录结构

一般来说,类型定义都是针对一个页面或者组件的(我们可以拿 CSS 样式文件的地位来做类比),即和组件(页面)所在文件在同一目录下,命名为 typings.d.ts:

└── src
    └── pages
        ├── detail
        │   ├── components
        │   │   └── Alert
        │   │       ├── index.jsx
        │   │       ├── style.scss
        │   │       └── typings.d.ts
        │   ├── index.jsx
        │   ├── style.scss
        │   └── typings.d.ts
        ├── home
        └── profile

书写细节

d.ts 文件和 ts 文件一个最大的不同就是前者无需导入,可以理解为全局变量,这就很容易导致同名类型定义的冲突干扰,为了尽可能地解决该问题,推荐使用 namespace (命名空间)语法来分组局部类型定义,还是以上面一小节的目录结构为例,Alert 组件的 typings.d.ts 可以这样写:

declare namespace Page.Detail.Components.Alert {
   interface AlertProps {
    /**
     * 警告内容
     */
    message: string;

    /**
     * 当关闭时做些什么
     */
    onClose: () => void;
  }
}

原则是根据其所属的页面、组件等层级关系划分,层级之间可以用点号隔开,以防止过多的嵌套影响美观。

其他的书写细节和 TypeScript 的写法别无二致,平时 ts 怎么写的,这里如法炮制即可。

消费方式

核心思路是通过编写 jsdoc 进行消费,下面提供几个不错的实践:

案例 1:React 函数式组件

下面的案例中使用了 React.FC 工具函数来实现。(提示:TypeScript 的各种内置 Utility Types 也是可以利用起来的)

/**
 * Alert 组件,用来展示警告信息,如:网络错误、服务器错误等
 *
 * @type {React.FC<Page.Detail.Components.Alert.AlertProps>}
 */
export const Alert = (props) => {
  const { message, onClose } = props;
  return <div>hello world!</div>;
};
案例 2:普通函数

对于一般函数,使用 @param 或者 @returns 注释描述该函数的入参和返回值。

/**
 * 获取用户信息描述
 *
 * @param {Page.Detail.Components.Alert.UserInfo} userInfo 用户信息
 * @return {string} 用户信息描述
 */
const getUserDescription = (userInfo) => {
  const {name, age} = userInfo;
  return `我是 ${name}, 今年 ${age} 岁`;
}
案例 3:一般变量

对于一般变量,可以使用 @types 注释在该变量顶部描述:

/** @type {Page.Detail.Components.Alert.UserInfo} */
const userInfo = {
  age: 18,
  name: '张三',
}

// 或者也可以这样写
const user = /** @type {Page.Detail.Components.Alert.UserInfo} */ {
  age: 18,
  name: '张三',
};

// 在 return 语句中
return /** @type {Page.Detail.Components.Alert.UserInfo} */ {
  age: 18,
  name: '张三',
};

最后,配合 VSCode、WebStorm 等现代 IDE,你就可以享受到类型提示了!

image

🚨 使用时的陷阱

无法识别 d.ts 中定义的 namespace

先引入一个问题:

已知某项目是一个巨大的 monorepo,请看它的 tsconfig.json,它有什么潜在的问题?

{
  "compilerOptions": {
    "outDir": "./FAKE_DIR",
    "target": "ESNext",
    "module": "CommonJS",
    "esModuleInterop": true,
    "allowJs": true,
    "checkJs": true,
    "jsx": "react"
  },
  "include": [
    "./packages/**/*",
    "./typings"
  ]
}
  • 大部分情况下,monorepo 仓库下的构建产物(大部分是特别长的 JavaScript 文件)都会被打包到各自 package 下面,很容易被 include 字段匹配

  • 一旦开启 allowJs = true,TypeScript Server 就会去检查这些不必要的 JavaScript 文件,从而触发性能阈值失去某些 feature,例如我们上文的 JavaScript Check 能力。

image

那么如何去看是什么文件造成了此问题?这里提供一个非常优雅的解法:

  • 首先打开 TS Sever Log:

image

  • 在大概第三十行左右的位置就可以看到 TypeScript Checker log 了所有被解析的 build 产物,这些都应该被排除在外:

image

image

在上面这个 Case 中,就可以通过 exclude 字段排除掉所有的 build 目录来提高性能:

{
  "compilerOptions": {
   // 略去
  },
  "include": [
    "./packages/**/*",
    "./typings"
  ],
  "exclude": [
    "**/build/**"
  ]
}