最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • TypeScript 渐进迁移指南

    正文概述 掘金(LeanCloud)   2021-01-14   502

    Nathaniel 原作,翻译转载自 New Frontend 。

    我在大概一年前写了一篇如何把 Node.js 项目从 JavaScript 迁移到 TypeScript 的指南。指南的阅读量超过了七千,不过其实当时我对 JavaScript 和 TypeScript 的了解并不深入,把重心更多地放到特定工具上,而没怎么从全局着手。最大的问题是我没有提供迁移大型项目的解决方案。显然,大型项目不可能在短时间内重写一切。因此,我很想分享下我最近学到的迁移项目到 TypeScript 的主要经验。

    迁移一个包含成千上百个文件的大型项目可能比你想象得要容易。整个过程主要分 3 步。

    注意:本文假定你已经有一定的 TypeScript 基础,同时使用 Visual Studio Code,否则,一些地方可能不一定直接适用。

    相关代码:github.com/llldar/migr…

    开始引入类型

    花了 10 个小时使用 console.log 排查问题后,你终于修复了 Cannot read property 'x' of undefined 问题,出现这个问题的原因是调用了可能为 undefined 的某个方法,给了你一个「惊喜」!你暗暗发誓,一定要把整个项目迁移到 TypeScript。但是看了看 libutilcomponents 文件夹里上万个 JavaScript 文件,你对自己说:「等以后吧,等我有空的时候。」当然那一天永远也不会到来,因为总有各种酷炫的新特性等着加到应用,客户也不会因为项目是用 TypeScript 写的就出大价钱。

    如果我告诉你,你可以增量迁移到 TypeScript 并立刻从中受益呢?

    添加神奇的 d.ts

    d.ts 是 TypeScript 的类型声明文件,其中声明了代码中用到的对象和函数的各种类型,不包含任何具体的实现。

    假定你在写一个即时通讯应用,在 user.js 文件里有一个 user 变量和一些数组:

    const user = {
      id: 1234,
      firstname: 'Bruce',
      lastname: 'Wayne',
      status: 'online',
    };
    
    const users = [user];
    
    const onlineUsers = users.filter((u) => u.status === 'online');
    
    console.log(
      onlineUsers.map((ou) => `${ou.firstname} ${ou.lastname} is ${ou.status}`)
    );
    

    那么对应的 user.d.ts 会是:

    export interface User {
      id: number;
      firstname: string;
      lastname: string;
      status: 'online' | 'offline';
    }
    

    然后 message.js 里定义了一个函数 sendMessage

    function sendMessage(from, to, message)
    

    那么 message.d.ts 中相应的类型会是:

    type sendMessage = (from: string, to: string, message: string) => boolean
    

    不过,sendMessage 也许没那么简单,参数的类型可能更复杂,也可能是一个异步函数。

    你可以使用 import 引入其他文件中定义的复杂类型,保持类型文件简单明了,避免重复。

    import { User } from './models/user';
    type Message = {
      content: string;
      createAt: Date;
      likes: number;
    }
    interface MessageResult {
      ok: boolean;
      statusCode: number;
      json: () => Promise<any>;
      text: () => Promise<string>;
    }
    type sendMessage = (from: User, to: User, message: Message) => Promise<MessageResult>
    

    注意:我这里同时使用了 typeinterface,这是为了展示如何使用它们。你在项目中应该主要使用其中一种。

    连接类型

    现在已经有类型了,如何搭配 js 文件使用呢?

    大体上有两种方式:

    Jsdoc typedef import

    假设同一文件夹下有 user.d.ts,可以在 user.js 文件中加入以下注释:

    /**
     * @typedef {import('./user').User} User
     */
    
    /**
     * @type {User}
     */
    const user = {
      id: 1234,
      firstname: 'Bruce',
      lastname: 'Wayne',
      status: 'online',
    };
    
    /**
     * @type {User[]}
     */
    const users = [];
    
    // onlineUser 的类型会被自动推断为 User[]
    const onlineUsers = users.filter((u) => u.status === 'online');
    
    console.log(
      onlineUsers.map((ou) => `${ou.firstname} ${ou.lastname} is ${ou.status}`)
    );
    

    确保 d.ts 文件中有相应的 importexport 语句,这一方式才能正确工作。否则,最终会得到 any 类型,显然 any 类型不会是你想要的。

    三斜杠指令

    在无法使用 import 的场景下,三斜杠指令是导入类型的经典方式。

    注意,你可能需要在 eslint 配置文件中加入以下内容以免 eslint 把三斜杠指令视为错误:

    {
      "rules": {
        "spaced-comment": [
          "error",
          "always",
          {
            "line": {
              "markers": ["/"]
            }
          }
        ]
      }
    }
    

    假设 message.jsmessage.d.ts 在同一文件夹下,可以在 message.js 文件中加入以下三斜杠指令:

    /// <reference path="./models/user.d.ts" /> (仅当使用 user 类型时才加这一行)
    /// <reference path="./message.d.ts" />
    

    然后给 sendMessage 函数加上以下注释:

    /**
    * @type {sendMessage}
    */
    function sendMessage(from, to, message)
    

    接着你会发现 sendMessage 有了正确的类型,IDE 能自动补全 fromtomessage 和函数的返回类型。

    或者你也可以这么写:

    /**
    * @param {User} from
    * @param {User} to
    * @param {Message} message
    * @returns {MessageResult}
    */
    function sendMessage(from, to, message)
    

    这是 jsDoc 书写函数签名的风格,肯定没有上一种写法那么简短。

    使用三斜杠指令时,应该在 d.ts 文件中移除 importexport 语句,否则无法工作。如果你需要从其他文件中引入类型,可以这么写:

    type sendMessage = (
      from: import("./models/user").User,
      to: import("./models/user").User,
      message: Message
    ) => Promise<MessageResult>;
    

    这一差别背后的原因是 TypeScript 把不含 importexport 语句的 d.ts 文件视作环境(ambient)模块声明,包含 importexport 语句的则视为普通模块文件,而不是全局声明,所以无法用于三斜杠指令。

    注意,在实际项目中,选择以上两种方式中的一种,不要混用。

    自动生成 d.ts

    如果项目的 JavaScript 代码中已经有大量 jsDoc 注释,那么你有福了,只需以下一行命令就能自动生成类型声明文件:

    npx typescript src/**/*.js --declaration --allowJs --emitDeclarationOnly --outDir types
    

    以上命令中,所有 js 文件在 src 文件夹下,输出的 d.ts 文件位于 types 文件夹下。

    babel 配置(可选)

    如果项目使用 babel,那么需要在 babelrc 里加上:

    {
      "exclude": ["**/*.d.ts"]
    }
    

    否则 *.d.ts 文件会被编译为 *.d.js 文件,这毫无意义。

    现在你应该就能享受到 TypeScript 的益处了(自动补全),无需额外配置 IDE,也不用修改 js 代码的逻辑。

    类型检查

    如果项目中 70% 以上的代码都经过以上步骤迁移后,你可以考虑开启类型检查,进一步帮助检测代码中的小错误和问题。别担心,你仍将继续使用 JavaScript,也就是说不用改动构建过程,也不用换库。

    开启类型检查的主要步骤是在项目中加上 jsconfig.json。例如:

    {
      "compilerOptions": {
        "module": "commonjs",
        "target": "es5",
        "checkJs": true,
        "lib": ["es2015", "dom"]
      },
      "baseUrl": ".",
      "include": ["src/**/*"],
      "exclude": ["node_modules"]
    }
    

    关键在于 checkJs 需要为真,这就为所有项目开启了类型检查。

    开启后可能会碰到一大堆报错,可以逐一修正。

    渐进类型检查

    // @ts-nocheck

    如果你希望以后再修复一些文件的类型问题,可以在文件头部加上 // @ts-nocheck,TypeScript 编译器会忽略这些文件。

    // @ts-ignore

    如果只想忽略某行而不是整个文件的话,可以使用 // @ts-ignore。加上这个注释后,类型检查会忽略下一行。

    使用这两个标记可以让你慢慢修正类型检查错误。

    第三方库

    维护良好的库

    如果用的是流行的库,那 DefinitelyTyped 上多半已经有类型定义了,只需运行以下命令:

    yarn add @types/your_lib_name --dev
    

    npm i @types/your_lib_name --save-dev
    

    注意:如果库属于某组织,库名中包含 @/,那么在安装相应的类型定义文件时需要移除 @/,并在组织名后加上 __,例如 @babel/core 改为 babel__core

    纯 JS 库

    如果用了一个作者 10 年前就已经停止更新的 js 库怎么办?大多数 npm 模块仍然使用 JavaScript,没有类型信息。添加 @ts-ignore 看起来不是一个好主意,因为你希望尽可能地确保类型安全。

    那你就需要通过创建 d.ts 文件增补模块定义,建议创建一个 types 文件夹,加入自己的类型定义。然后就可以享受类型安全检查了。

    declare module 'some-js-lib' {
      export const sendMessage: (
        from: number,
        to: number,
        message: string
      ) => Promise<MessageResult>;
    }
    

    完成这些步骤后,类型检查应该能很好地工作,可以避免代码出现很多小错误。

    类型检查升级

    修复 95% 以上类型检查错误并确保每个库都有相应的类型定义后,你可以进行最后一步:正式把整个项目的代码迁移到 TypeScript。

    注意:我上一篇指南中提到的一些细节这里就不讲了。

    把所有文件改为 .ts 文件

    现在是时候把 d.ts 文件和 js 文件合并了。由于几乎所有的类型检查错误都已修正,类型检查已经覆盖所有模块,基本上只需要把 require 改成 import 然后把代码和类型定义都放到 ts 文件中。完成之前的工作后,这一步相当简单。

    把 jsconfig 改为 tsconfig

    现在我们需要的是 tsconfig.json 而不是 jsconfig.json

    tsconfig.json 的例子:

    前端项目

    {
      "compilerOptions": {
        "target": "es2015",
        "allowJs": false,
        "esModuleInterop": true,
        "allowSyntheticDefaultImports": true,
        "noImplicitThis": true,
        "strict": true,
        "forceConsistentCasingInFileNames": true,
        "module": "esnext",
        "moduleResolution": "node",
        "resolveJsonModule": true,
        "isolatedModules": true,
        "noEmit": true,
        "jsx": "preserve",
        "lib": ["es2020", "dom"],
        "skipLibCheck": true,
        "typeRoots": ["node_modules/@types", "src/types"],
        "baseUrl": ".",
      },
      "include": ["src"],
      "exclude": ["node_modules"]
    }
    

    后端项目

    {
      "compilerOptions": {
          "sourceMap": false,
          "esModuleInterop": true,
          "allowJs": false,
          "noImplicitAny": true,
          "skipLibCheck": true,
          "allowSyntheticDefaultImports": true,
          "preserveConstEnums": true,
          "strictNullChecks": true,
          "resolveJsonModule": true,
          "moduleResolution": "node",
          "lib": ["es2018"],
          "module": "commonjs",
          "target": "es2018",
          "baseUrl": ".",
          "paths": {
              "*": ["node_modules/*", "src/types/*"]
          },
          "typeRoots": ["node_modules/@types", "src/types"],
          "outDir": "./built",
      },
      "include": ["src/**/*"],
      "exclude": ["node_modules"]
    }
    

    因为这样修改后类型检查会变得更严格,所以可能需要修复一些额外的类型错误。

    修改 CI/CD 和构建流程

    改到 TypeScript 后需要在构建流程中生成可运行的代码,通常在 package.json 中加上这一行就行:

    {
      "scripts":{
        "build": "tsc"
      }
    }
    

    不过,前端项目通常用了 babel,你需要这样设置项目:

    {
      "scripts": {
        "build": "rimraf dist && tsc --emitDeclarationOnly && babel src --out-dir dist --extensions .ts,.tsx && copyfiles package.json LICENSE.md README.md ./dist"
      }
    }
    

    别忘了改入口文件,比如:

    {
      "main": "dist/index.js",
      "module": "dist/index.js",
      "types": "dist/index.d.ts",
    }
    

    好了,万事俱备。

    注意,dist 需要改成你实际使用的目录。

    结语

    恭喜,代码现在迁移到了 TypeScript,有严格的类型检查保证。现在可以享受 TypeScript 带来的所有好处,比如自动补全、静态类型、esnext 语法、对大型项目友好。开发体验大大提升,维护成本大大降低。编写项目代码不再是痛苦的过程,再也不会碰到 Cannot read property 'x' of undefined 报错。

    替代方案:

    如果你希望一下子迁移整个项目到 TypeScript,可以参考 airbnb 团队的指南。


    起源地下载网 » TypeScript 渐进迁移指南

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元