Skip to content
Return to top
On this page

# 支持 Typescript 和 JSX #

在 Vue 中开启 Typescript 和 JSX 支持并不是一件容易的事情。 因此我们单独出一份文档来说明一下。

## 依赖 #

如果你想要使用 `TSX`/`JSX` 开发，并且获取到更好的 `Typescript` 类型检查，需要安装以下依赖:

shell

    $ pnpm add babel-preset-wakeadmin @wakeadmin/h @wakeadmin/demi vue-tsc -D

    # 升级 @wakeadmin/* 相关依赖到最新版本
    $ pnpm up -r -L \"@wakeadmin/*\"

- `babel-preset-wakeadmin` 使用 react jsx 的标准语法来转换 JSX。
- `@wakeadmin/h` 更好地支持 react-jsx 的标准 JSX 语法，没有语法糖，更接近 React 的开发体验。兼容 Vue 2/3。详见下文
- `@wakeadmin/demi` fork from `vue-demi`, 修复了一些问题。可以完全取代 `vue-demi`
- `vue-tsc` Volar 底层依赖的 Typescript 编译器。这里主要用于类型检查

建议将 @wakeadmin/* 相关库都升级到最新版本

## 构建 #

首先根据你使用的构建工具，配置相关的 Typescript 构建支持：

- `Vite`: 内置支持转换, 但是类型检查需要借助 `vue-tsc`
- `Vue CLI`: 卸载掉 `@vue/cli-plugin-typescript`, 并安装 `@wakeadmin/vue-cli-plugin-typescript`。这个插件底层基于 `esbuild` 进行编译，不会进行类型检查, 因此还是需要借助 `vue-tsx`, 详见下文。

💥 不推荐使用 `@vue/cli-plugin-typescript` 插件:

为什么不使用它？ Vue 官方也不推荐使用它。一个比较重要的问题是，它的执行结果未必和 IDE 一致，异常也很难排查。性能也较差、无法同 vue-tsc 一样真正检查 `*.vue` 文件。

## 配置 #

接着配置 tsconfig.json:

json

    {
      "compilerOptions": {
        "types": ["@wakeadmin/demi"]
      },
      // 如果是 vue 2, 则加上以下配置
      "vueCompilerOptions": {
        "target": 2.7,
        // 转换 template 为 jsx 模式，目前而言好处就是检查更加严格, 而且对泛型组件的支持较好
        "jsxTemplates": true
      }
    }

接着，配置一个 `src/env.d.ts`(旧的项目可能已存在, 比如 vue-cli, `shims-tsx.d.ts`、`shims-vue.d.ts`, 将这些文件删掉) 文件，让 TypeScript 标准的类型检查器可以识别 `*.vue` 文件:

ts

    // env.d.ts
    // 你可能把旧的 declare module '*.vue' 移除
    declare module '*.vue' {
      import type { DefineComponent } from 'vue';
      const component: DefineComponent<{}, {}, any>;
      export default component;
    }

`.vue` 文件, 像 `.css` 这些静态资源文件一样, 标准的 Typescript 是无法识别里面的类型的, 当然装了 Volar 插件之后, Valor 可以做到。但是仅在 IDE 层面，如果你想要在构建时/CI 时进行类型检查，可以用 vue-tsc

## IDE 类型提示 #

IDE 上推荐使用 `Valor` 插件，并禁用掉 `Vetur` 插件。在 VSCode 中, 你可以安装一下两个插件:

- Vue Language Features: Vue, Vitepress, petite-vue language support extension for VSCode
- TypeScript Vue Plugin VSCode extension to support Vue in TS server

## 开启类型检查 #

最后，如果想要对类型进行检查，推荐使用 `vue-tsc`:

json

    // package.json
    {
      "scripts": {
        "prebuild": "vue-tsc --noEmit"
      }
    }

如果使用了惟客云自动化检查工具, 可以这样配置：

json

    // .standard.jsonc
    {
      // 执行 Typescript 类型检查
      "typescriptEnable": true,

      // typescript 检查命令
      "typescriptCmd": "vue-tsc --noEmit"
    }

## 更好地支持 JSX/TSX #

大部分场景，我们推荐你使用 Vue 的 SFC + setup + TypeScript 来编写组件。

然而，在你们使用 `@wakeadmin/components` 时，为了灵活定义组件库，你会经常用到 JSX。

假设你的项目使用是 Vue-cli, 第一步先修改 `babel.config.js`

js

    module.exports = {
      // 关闭 vue 默认的 jsx 转换， 统一使用标准的 JSX
      presets: [['@vue/cli-plugin-babel/preset', { jsx: false }], 'babel-preset-wakeadmin'],
    };

接着修改 `tsconfig.json` 配置:

json

    {
      "compilerOptions": {
        "jsx": "react-jsx",
        "jsxImportSource": "@wakeadmin/h",
        "types": ["@wakeadmin/demi"],
      },
    }

这里，我们使用 `@wakeadmin/h` 来编写 JSX。好处是：

1. 不管你用的是 Vue 2, 还是 Vue 3, 使用 `@wakeadmin/h` 可以提供一致的编写方式, 更接近我们在 React 上的使用习惯。

   - Vue 2 / 3 JSX 书写上相差非常大
   - Vue 官方的 JSX 库携带了很多语法糖。这依赖于 Babel 的转换，这意味着你无法直接使用 esbuild、Typescript 这类工具进行编译。

2. 除此之外，`@wakeadmin/h` 也优化了 Vue JSX 在 Typescript 支持上的一些问题。

使用示例：

jsx

    <div onClick={handleClick} class="hello" />; // 使用 on* 的语法进行事件监听
    <div onClick={handleClick} class={[hello, { active: isActive }]} style={{ color: 'red' }} />;

    // 插槽的使用，使用 v-slots
    <Tooltip v-slots={{ content: <div>hello</div>, named: scope => <div>命名插槽</div> }}>
      <span class="fat-actions__btn">{content}</span>
    </Tooltip>;

    // 指令：https://vuejs.org/api/render-function.html#withdirectives
    <div {...withDirectives([[vLoading, loading.value]])}>加载中</div>;

## ESLint 适配 #

你可以使用 `wkstd init` 来初始化 eslint 的配置, 典型的 ESlint 配置如下：

js

    module.exports = {
      extends: ['wkts', 'wkvue'],
      plugins: [],
      globals: {},
      rules: {
        'vue/no-deprecated-slot-attribute': 'off',
      },
      parser: 'vue-eslint-parser',
      // 为了支持 Typescript 需要配置 '@typescript-eslint/parser'
      parserOptions: {
        parser: '@typescript-eslint/parser',
        project: 'tsconfig.json',
        sourceType: 'module',
        ecmaVersion: 'latest',
        extraFileExtensions: ['.vue'],
      },
      env: {
        browser: true,
        es2020: true,
      },
      // 检查 .tsx/.ts 文件
      overrides: [
        {
          files: ['*.ts', '*.tsx'],
          rules: {
            // 使用 typescript 检查
          },
          parser: '@typescript-eslint/parser',
          parserOptions: {
            warnOnUnsupportedTypeScriptVersion: true,
            ecmaVersion: 'latest',
            lib: ['esNext'],
            project: './tsconfig.json',
          },
        },
      ],
    };

然而可能不会像你想象的那么顺利。

❌ 异常 1: Parsing error: "parserOptions.project" has been set for @typescript-eslint/parser. The file does not match your project config: packages/eslint-config-wkvue/tests/vue2/ts/.eslintrc.js. The file must be included in at least one of the projects provided

原因分析： 我们在上面的 eslint 配置中指定了 `@typescript-eslint/parser` 作为所有文件 parser。然而 `@typescript-eslint/parser`, 如果指定的文件不再 tsconfig.json 的覆盖范围之内就会出现该问题。

解决办法：

- ① (不推荐)在 tsconfig.json include 进来, 比如:

  json

      // tsconfig.json
      {
        "include": [
          "types.d.ts",
          "src/**/*.vue",
          "scripts",
          "src/**/*.ts",
          "src/**/*.tsx",
          "src/**/*.js",
          "tailwind.config.js",
          ".eslintrc.js",
          "vue.config.js"
        ],
        "exclude": ["node_modules"]
      }

  对于旧的项目，这些文件很多。这种解法比较蠢，而且会影响 Typescript 的检查效率，甚至会污染检查的结果。如果真的非得用这种方式，可以另起一个 `tsconfig.eslint.json` 的配置文件，并将 `parserOptions.project` 指向这个文件。

- ② 为不同的文件支持不同的 parser

  `tsconfig.json` 应该专注于应该检查的文件：

  json

      {
        "compilerOptions": {
          // 支持引用 js 模块
          "allowJS": true,

          // 对于旧项目不建议打开, 会有很多报错
          "checkJS": false
        },
        // 检查 .vue 文件
        "include": ["src/**/*", "src/**/*.vue"]
      }

  接着配置 eslintrc 对不同的文件使用不同的 parser 进行处理：

  js

      module.exports = {
        extends: ['wkts', 'wkvue'],
        parser: 'vue-eslint-parser',
        parserOptions: {
          parser: {
            js: '@babel/eslint-parser',
            jsx: '@babel/eslint-parser',
            ts: '@typescript-eslint/parser',
            tsx: '@typescript-eslint/parser',
          },
          project: './tsconfig.json',
          sourceType: 'module',
          extraFileExtensions: ['.vue'],
        },
      };

- ③ (推荐) 一个更简单的办法是开启 typescript-eslint 的 `createDefaultProgram` 选项：

  js

      module.exports = {
        extends: ['wkts', 'wkvue'],
        parser: 'vue-eslint-parser',
        parserOptions: {
          parser: '@typescript-eslint/parser',
          project: './tsconfig.json',
          sourceType: 'module',
          createDefaultProgram: true,
          extraFileExtensions: ['.vue'],
        },
      };

  简单的代价是性能会稍差一些。

❌ 异常 2: 如果你要在 Vue SPA 中使用 `<script lang="tsx"></script>`， Eslint 可能会报错，你需要以下配置：

js

    // 支持 .vue 文件中 包含 jsx
    const ts = require('typescript');

    const { ensureScriptKind } = ts;
    ts.ensureScriptKind = function (fileName, ...args) {
      if (fileName.endsWith('.vue')) {
        return ts.ScriptKind.TSX;
      }
      return ensureScriptKind.call(this, fileName, ...args);
    };

    module.exports = {
      extends: ['wkts', 'wkvue'],
      // ....  eslint 配置
    };

## 老项目中启用 TypeScript #

老项目(假设是 Vue 2)中也可以按照上文的配置开启 Typescript。有以下几个要点

- 开启 `tsconfig.json` 的 allowJS, 而不是 checkJS。这表示允许和 Javascript 混用，但不检查 Javascript。如果开启 checkJS 可能有成吨的异常抛出来。

- 如何扩展 Vue 的类型？比如全局方法、全局对象、全局组件等等。 Vue 2/3 定义方式有点区别，这里展示 Vue 2 的使用方式，你也可以参考相关的第三方库。以 vue-router 为例

  扩展全局组件实例成员:

  typescript

      declare module 'vue/types/vue' {
        interface Vue {
          $router: VueRouter;
          $route: Route;
        }
      }

  扩展 optional API

  ts

      declare module 'vue/types/options' {
        interface ComponentOptions<V extends Vue> {
          router?: VueRouter;
          beforeRouteEnter?: NavigationGuard<V>;
          beforeRouteLeave?: NavigationGuard<V>;
          beforeRouteUpdate?: NavigationGuard<V>;
        }
      }

  扩展全局组件:

  ts

      declare module 'vue' {
        export interface GlobalComponents {
          RouterLink: typeof import('vue-router')['RouterLink'];
          RouterView: typeof import('vue-router')['RouterView'];
        }
      }

`element-ui` 同理：

tsx

    // 注意 element-ui 类型基本上是残废的，无法正常推断 props 类型
    declare module 'vue' {
      import element from 'element-ui';

      type TypeofElementExpose = typeof element;
      type KeyOfElementExpose = keyof TypeofElementExpose;
      type KeyofComponent = Exclude<KeyOfElementExpose, 'version' | 'install'>;

      type ElementComponents = {
        [K in KeyofComponent as `El${K}`]: TypeofElementExpose[K];
      };

      export interface GlobalComponents extends ElementComponents {
        RouterView: typeof import('vue-router').RouterView;
      }
    }