• 【长文详解】TypeScript、Babel、webpack以及IDE对TS的类型检查


    只要接触过ts的前端同学都能回答出ts是js超集,它具备静态类型分析,能够根据类型在静态代码的解析过程中对ts代码进行类型检查,从而在保证类型的一致性。那,现在让你对你的webpack项目(其实任意类型的项目都同理)加入ts,你知道怎么做吗?带着这个问题,我们由浅入深,逐步介绍TypeScriptBabel以及我们日常使用IDE进行ts文件类型检查的关系,让你今后面对基于ts的工程能够做到游刃有余。

    TypeScript基本认识

    原则1:主流的浏览器的主流版本只认识js代码

    原则2:ts的代码一定会经过编译为js代码,才能运行在主流浏览器上

    要编译ts代码,至少具备以下几个要素:

    1. ts源代码
    2. ts编译器
    3. ts编译器所需要的配置(默认配置也是配置)

    编译TS的方式

    目前主流的ts编译方案有2种,分别是官方tsc编译、babel+ts插件编译。

    官方tsc编译器

    对于ts官方模式来说,ts编译器就是tsc(安装typescript就可以获得),而编译器所需的配置就是tsconfig.json配置文件形式或其他形式。ts源代码经过tsc的编译(Compile),就可以生成js代码,在tsc编译的过程中,需要编译配置来确定一些编译过程中要处理的内容。

    010-ts-compile-flow

    我们首先准备一个ts-demo,该demo中有如下的结构:

    ts-demo
     |- packages.json
     |- tsconfig.json
     |- src
        |- index.ts
    

    安装typescript

    yarn add -D typescript
    

    package.json

    {
      "name": "ts-demo",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "build-ts": "tsc"
      },
      "author": "",
      "license": "MIT",
      "devDependencies": {
        "typescript": "^4.7.4"
      }
    }
    

    tsconfig.js(对于这个简单的tsconfig,我不再赘述其配置的含义。)

    {
      "compilerOptions": {
        "module": "commonjs",
        "rootDir": "./src",
        "outDir": "./dist"
      }
    }
    

    index.ts

    interface User {
        name: string;
        age: number;
    }
    const userToString = (user: User) => `${user.name}@${user.age}`;
    export {userToString, User};
    

    此时,我们只需要运行yarn build-ts就可以将我们的index.ts编译为index.js:

    commonjs模块化方式产物:

    "use strict";
    exports.__esModule = true;
    exports.userToString = void 0;
    var userToString = function (user) { return "".concat(user.name, "@").concat(user.age); };
    exports.userToString = userToString;
    

    可以看到,原本index.ts编译为index.js的产物,使用了commonjs模块化方案(tsconfig里面配置模块化方案是"commonjs",编译后的代码可以看到"exports"的身影);倘若我们将模块化方案改为ESM(ES模块化)的es:"module": "es6",编译后的产物依然是index.js,只不过内容采用了es6中的模块方案。

    es6模块化方式产物:

    var userToString = function (user) {
      return "".concat(user.name, "@").concat(user.age);
    };
    export {userToString};
    

    说了这么多,只是想要告诉各位同学,ts无论有多么庞大的语法体系,多么强大的类型检查,最终的产物都是js

    此外,ts中的模块化,不能和js中的模块化混为一谈。js中的模块化方案很多(es6、commonjs、umd等等),所以ts本身在编译过程中,需要指定一种js的模块化表达,才能编译为对应的代码。也就是说,在ts中的import/export,不能认为和es6的import/export是一样的,他们是完全不同的两个体系!只是语法上类似而已。

    babel+ts插件

    如前文所述

    ts源代码经过tsc的编译(Compile),就可以生成js代码,在tsc编译的过程中,需要编译配置来确定一些编译过程中要处理的内容。

    那么是不是说,编译器这块是不是有其他的代替呢?ts源码经过某种其他的编译器编译后,生成目标js代码。答案是肯定的:babel。

    我们准备一个ts-babel-demo:

    ts-babel-demo
     |- packages.json
     |- .babelrc
     |- src
        |- index.ts
    

    依赖添加:

     yarn add -D @babel/core @babel/cli
     yarn add -D @babel/preset-env @babel/preset-typescript
     yarn add -D @babel/plugin-proposal-class-properties @babel/plugin-proposal-object-rest-spread
    

    package.json:

    {
      "name": "ts-babel-demo",
      "version": "1.0.0",
      "main": "index.js",
      "license": "MIT",
      "private": true,
      "scripts": {
        "build": "babel src -d dist -x '.ts, .tsx'"
      },
      "devDependencies": {
        "@babel/cli": "^7.18.10",
        "@babel/core": "^7.18.10",
        "@babel/plugin-proposal-class-properties": "^7.18.6",
        "@babel/plugin-proposal-object-rest-spread": "^7.18.9",
        "@babel/preset-env": "^7.18.10",
        "@babel/preset-typescript": "^7.18.6"
      }
    }
    

    .babelrc

    {
      "presets": [
        "@babel/preset-env",
        "@babel/preset-typescript"
      ],
      "plugins": [
        "@babel/plugin-proposal-object-rest-spread",
        "@babel/plugin-proposal-class-properties"
      ]
    }
    

    index.ts和ts-demo保持一致。

    完成基础的项目搭建以后,我们执行yarn build

    ~/Projects/web-projects/ts-babel-demo > yarn build
    yarn run v1.22.17
    $ babel src -d dist -x '.ts, .tsx'
    Successfully compiled 1 file with Babel (599ms).
    Done in 4.05s.
    

    可以看到项目dist目录下出现了编译好的js代码:

    "use strict";
    
    Object.defineProperty(exports, "__esModule", {
      value: true
    });
    exports.userToString = void 0;
    
    var userToString = function userToString(user) {
      return "".concat(user.name, "@").concat(user.age);
    };
    
    exports.userToString = userToString;
    

    可以看到和使用tsc编译为commonjs效果是一样。

    回顾这个项目,其实按照我们之前的思路来梳理:

    1. ts源文件(src/index.ts)
    2. ts的编译器(babel)
    3. 编译配置(.babelrc)

    020-babel-compile-flow

    了解babel机制

    如果对于babel不太熟悉,可能对上述的一堆依赖感到恐惧:

     yarn add -D @babel/core @babel/cli
     yarn add -D @babel/preset-env @babel/preset-typescript
     yarn add -D @babel/plugin-proposal-class-properties @babel/plugin-proposal-object-rest-spread
    

    这里如果读者有时间,我推荐这篇深入了解babel的文章:一口(很长的)气了解 babel - 知乎 (zhihu.com)。当然,如果这口气憋不住(哈哈),我做一个简单摘抄:

    babel 总共分为三个阶段:解析,转换,生成。

    babel 本身不具有任何转化功能,它把转化的功能都分解到一个个 plugin 里面。因此当我们不配置任何插件时,经过 babel 的代码和输入是相同的。

    插件总共分为两种:

    • 当我们添加 语法插件 之后,在解析这一步就使得 babel 能够解析更多的语法。(顺带一提,babel 内部使用的解析类库叫做 babylon,并非 babel 自行开发)

    举个简单的例子,当我们定义或者调用方法时,最后一个参数之后是不允许增加逗号的,如 callFoo(param1, param2,) 就是非法的。如果源码是这种写法,经过 babel 之后就会提示语法错误。

    但最近的 JS 提案中已经允许了这种新的写法(让代码 diff 更加清晰)。为了避免 babel 报错,就需要增加语法插件 babel-plugin-syntax-trailing-function-commas

    • 当我们添加 转译插件 之后,在转换这一步把源码转换并输出。这也是我们使用 babel 最本质的需求。

    比起语法插件,转译插件其实更好理解,比如箭头函数 (a) => a 就会转化为 function (a) {return a}。完成这个工作的插件叫做 babel-plugin-transform-es2015-arrow-functions

    同一类语法可能同时存在语法插件版本和转译插件版本。如果我们使用了转译插件,就不用再使用语法插件了。

    简单来讲,使用babel就像如下流程:

    源代码 =babel=> 目标代码
    

    如果没有使用任何插件,源代码和目标代码就没有任何差异。当我们引入各种插件的时候,就像如下流程一样:

    源代码
    |
    进入babel
    |
    babel插件1处理代码:移除某些符号
    |
    babel插件2处理代码:将形如() => {}的箭头函数,转换成function xxx() {}
    |
    目标代码
    

    因为babel的插件处理的力度很细,我们代码的语法、语义内容规范有很多,如果我们要处理这些语法,可能需要配置一大堆的插件,所以babel提出,将一堆插件组合成一个preset(预置插件包),这样,我们只需要引入一个插件组合包,就能处理代码的各种语法、语义。

    所以,回到我们上述的那些@babel开头的npm包,再回首可能不会那么迷茫:

    @babel/core
    @babel/preset-env
    @babel/preset-typescript
    @babel/preset-react
    @babel/plugin-proposal-class-properties
    @babel/plugin-proposal-object-rest-spread
    
    • @babel/core毋庸置疑,babel的核心模块,实现了上述的流程运转以及代码语法、语义分析的功能;

    • @babel/cli则是我们可以在命令行使用babel命令;

    • plugin开头的就是插件,这里我们引入了两个:@babel/plugin-proposal-class-properties允许类具有属性)和@babel/plugin-proposal-object-rest-spread对象展开);

    • preset开头的就是预置组件包合集,其中@babel/preset-env表示使用了可以根据实际的浏览器运行环境,会选择相关的转义插件包,通过配置得知目标环境的特点只做必要的转换。如果不写任何配置项,env 等价于 latest,也等价于 es2015 + es2016 + es2017 三个相加(不包含 stage-x 中的插件);@babel/preset-typescript会处理所有ts的代码的语法和语义规则,并转换为js代码。

    关于babel编译ts,并不是所有的语法都支持,这里有一篇文章专门介绍了其中注意点:《TypeScript 和 Babel:美丽的结合》。

    webpack项目级TS使用

    前面的内容,我们已经介绍了将ts编译为js的两种方式(tsc、babel),但仅仅是简单将一个index.ts编译为index.js。实际上,对于项目级别的ts项目,还有很多需要了解的。接下来基于一个webpack项目来逐步介绍如何基于前文的两种方式来使用ts。

    对于webpack来说,至少需要读者了解到webpack的基本机制:概念 | webpack 中文文档 (docschina.org)

    简单来讲,webpack运行从指定的entry文件开始,从顶层开始分析依赖的内容,依赖的内容可以是任何的内容(只要是import的或require了的),而loader可以专门来处理各种类型的文件。

    webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效 模块,以供应用程序使用,以及被添加到依赖图中

    030-webpack-base-flow

    所以,当一个webpack项目是基于TS进行的时候,我们一定会有一个loader来处理ts(甚至是tsx)。当然,我们还是通过demo搭建来演示讲解。

    ts-loader

    mkdir webpack-ts-loader-demo && cd webpack-ts-loader-demo
    yarn init
    yarn add -D webpack webpack-cli
    yarn add -D ts-loader
    

    package.json

    {
      "name": "webpack-ts-loader-demo",
      "version": "1.0.0",
      "main": "index.js",
      "license": "MIT",
      "scripts": {
        "build": "webpack --config webpack.config.js"
      },
      "devDependencies": {
        "ts-loader": "^9.3.1",
        "webpack": "^5.74.0",
        "webpack-cli": "^4.10.0"
      }
    }
    

    webpack.config.js

    const {resolve} = require('path');
    module.exports = {
      entry: './src/index.ts',
      output: {
        path: resolve(__dirname, './dist'),
        filename: "index.js"
      },
      module: {
        rules: [
          {
            test: /\.ts/,
            loader: "ts-loader"
          }
        ]
      }
    };
    

    src/index.ts

    interface User {
        name: string;
        age: number;
    }
    const userToString = (user: User) => `${user.name}@${user.age}`;
    export {userToString, User};
    

    表面上,只需要上述三个文件,就可以编译ts文件,但是尝试运行yarn build会报错:

    Module build failed (from ./node_modules/ts-loader/index.js):
    Error: Could not load TypeScript. Try installing with `yarn add typescript` or `npm install typescript`. If TypeScript is installed globally, try using `yarn link typescript` or `npm link typescript`.
    

    通过报错很容易理解,我们没有安装typescript。为什么?因为ts-loader本身处理ts文件的时候,本质上还是调用的tsc,而tsc是typescript模块提供的。因此,我们只需要yarn add -D typescript即可(其实只需要开发依赖即可),但是紧接着又会有另外一个报错:

    ERROR in ./src/index.t
    Module build failed (from ./node_modules/ts-loader/index.js):
    Error: error while parsing tsconfig.json
    

    报错提醒我们,解析tsconfig的出错,不难理解,我们还没有配置tsconfig.json,因为tsc需要!所以,在我们项目中,加上tsconfig.json即可:

    tsconfig.json

    {
      "compilerOptions": {
        "module": "commonjs",
        "rootDir": "./src",
        "outDir": "./dist"
      }
    }
    

    配置完成以后,我们再次编译,发现可以编译成功,并且在dist目录下会有对应的js代码。

    然而,事情到这里就结束了吗?一个中大型的项目,必然有模块的引入,假如现在我们添加了个utils.ts

    export const hello = () => {
      return 'hello';
    }
    

    修改index.ts的代码,引入该hello方法,并使用:

    import {hello} from "./utils"; // 引入utils
    interface User {
        name: string;
        age: number;
    }
    const userToString = (user: User) => `${user.name}@${user.age}${hello()}`;
    export {userToString, User};
    

    再次运行yarn build,读者会发现还是会报错,但这一次的错误略有点出乎意料:

    Module not found: Error: Can't resolve './utils' in '/Users/w4ngzhen/Projects/web-projects/webpack-ts-loader-demo/src'
    resolve './utils' in '/Users/w4ngzhen/Projects/web-projects/webpack-ts-loader-demo/src'
    

    核心报错在于,webpack似乎无法找到utils这个模块。为什么呢?因为webpack默认是处理js代码的,如果你的代码中编写了import xxx from 'xxx',在没有明确指明这个模块的后缀的时候,webpack只会认为这个模块是以下几种:

    1. 无后缀文件
    2. js文件
    3. json文件
    4. wasm文件

    所以,你会看到具体一点的报错:

    resolve './utils' in '/Users/w4ngzhen/Projects/web-projects/webpack-ts-loader-demo/src'
      using description file: /Users/w4ngzhen/Projects/web-projects/webpack-ts-loader-demo/package.json (relative path: ./src)
        Field 'browser' doesn't contain a valid alias configuration
        using description file: /Users/w4ngzhen/Projects/web-projects/webpack-ts-loader-demo/package.json (relative path: ./src/utils)
          no extension
            Field 'browser' doesn't contain a valid alias configuration
            /Users/w4ngzhen/Projects/web-projects/webpack-ts-loader-demo/src/utils doesn't exist
          .js
            Field 'browser' doesn't contain a valid alias configuration
            /Users/w4ngzhen/Projects/web-projects/webpack-ts-loader-demo/src/utils.js doesn't exist
          .json
            Field 'browser' doesn't contain a valid alias configuration
            /Users/w4ngzhen/Projects/web-projects/webpack-ts-loader-demo/src/utils.json doesn't exist
          .wasm
            Field 'browser' doesn't contain a valid alias configuration
            /Users/w4ngzhen/Projects/web-projects/webpack-ts-loader-demo/src/utils.wasm doesn't exist
          as directory
    

    要想让webpack知道我们引入的utils是ts代码,方式为在webpack配置中,指明webpack默认处理的文件后缀:

    const {resolve} = require('path');
    module.exports = {
      // ... ...
      resolve: {
        // webpack 默认只处理js、jsx等js代码
        // 为了防止在import其他ts代码的时候,出现
        // " Can't resolve 'xxx' "的错误,需要特别配置
        extensions: ['.js', '.jsx', '.ts', '.tsx']
      },
      // ... ...
    };
    
    

    完成配置以后,我们就能够正确编译具备模块导入的ts代码了。

    综合来看,在基于ts-loader的webpack项目的解析流程处理如下。

    040-webpack-ts-loader-flow

    回顾一下webpack,它默认处理模块化js代码,比如index.js引用了utils.js(模块引用方式可以是commonjs,也可以是esModule形式),那么webpack从入口的index.js出发,来处理依赖,并打包为一个js(暂不考虑js拆分)。

    对于wepack+ts-loader的ts项目体系主要是通过ts-loader内部调用typescript提供的tsc,将ts代码编译为js代码(编译后的js代码依然是js模块化的形式),所以这个过程是需要tsconfig参与;等到tsc将整个所有的ts代码均编译为js代码以后,再整体交给webpack进行依赖分析并打包(也就进入webpack的默认处理流程)。

    细心的读者会发现这个过程有一个问题:由于先经过tsc编译后的js,又再被webpack默认的js处理机制进行分析并编译打包,这个过程一方面经过了两次编译(ts->标准模块化js->webpack模块体系js),那么如果ts项目特别大,模块特别多的时候,这个两次编译的过程会特别漫长!

    babel-loader

    前面我们简单介绍了如何使用babel对一份ts进行编译,那么在webpack中,如何使用babel呢?有的同学可能会想到这样操作步骤:我先用babel对ts进行编译为js,然后再利用webpack对js进行打包,这样的做法是可以的,但细想不就和上面的ts-loader一样的情况了吗?

    只要开发过基于webpack的现代化前端项目的同学,或多或少都看到过babel-loader的身影,他是个什么东西呢?先说结论吧,babel-loader是webpack和babel(由@babel/core和一堆预置集preset、插件plugins组合)的桥梁。

    050-webpack-babel-loader-flow

    根据这个图,同学可能觉得这不是和ts-loader的架构很像吗?webpack启动,遇到入口ts,匹配到babel-loader,babel-loader交给babel处理,处理完毕,回到webpack打包。但是使用babel进行ts处理,比起ts-loader更加高效。而关于这块的说明,我更加推荐读者阅读这篇文章 TypeScript 和 Babel:美丽的结合 - 知乎 (zhihu.com),简单来讲:

    警告!有一个震惊的消息,你可能想坐下来好好听下。

    Babel 如何处理 TypeScript 代码?它删除它

    是的,它删除了所有 TypeScript,将其转换为“常规的” JavaScript,并继续以它自己的方式愉快处理。

    这听起来很荒谬,但这种方法有两个很大的优势。

    第一个优势:️⚡️闪电般快速⚡️。

    大多数 Typescript 开发人员在开发/监视模式下经历过编译时间长的问题。你正在编写代码,保存一个文件,然后...它来了...再然后...最后,你看到了你的变更。哎呀,错了一个字,修复,保存,然后...啊。它只是慢得令人烦恼并打消你的势头。

    很难去指责 TypeScript 编译器,它在做很多工作。它在扫描那些包括 node_modules 在内的类型定义文件(*.d.ts),并确保你的代码正确使用。这就是为什么许多人将 Typescript 类型检查分到一个单独的进程。然而,Babel + TypeScript 组合仍然提供更快的编译,这要归功于 Babel 的高级缓存和单文件发射架构。

    让我们来搭建一个项目来复习这一过程吧:

    mkdir webpack-babel-loader-demo && cd webpack-babel-loader-demo
    yarn init
    yarn add -D webpack webpack-cli
    yarn add -D babel-loader
    yarn add -D @babel/core
    yarn add -D @babel/preset-env @babel/preset-typescript
    yarn add -D @babel/plugin-proposal-class-properties @babel/plugin-proposal-object-rest-spread
    

    package.json

    {
      "name": "webpack-babel-loader-demo",
      "version": "1.0.0",
      "main": "index.js",
      "license": "MIT",
      "scripts": {
        "build": "webpack --config webpack.config.js" 
      },
      "devDependencies": {
        "@babel/core": "^7.18.13",
        "@babel/plugin-proposal-class-properties": "^7.18.6",
        "@babel/plugin-proposal-object-rest-spread": "^7.18.9",
        "@babel/preset-env": "^7.18.10",
        "@babel/preset-typescript": "^7.18.6",
        "babel-loader": "^8.2.5",
        "webpack": "^5.74.0",
        "webpack-cli": "^4.10.0"
      }
    }
    

    webpack.config.js

    const {resolve} = require('path');
    module.exports = {
      entry: './src/index.ts',
      output: {
        path: resolve(__dirname, './dist'),
        filename: "index.js"
      },
      resolve: {
        // webpack 默认只处理js、jsx等js代码
        // 为了防止在import其他ts代码的时候,出现
        // " Can't resolve 'xxx' "的错误,需要特别配置
        extensions: ['.js', '.jsx', '.ts', '.tsx']
      },
      module: {
        rules: [
          {
            test: /\.ts/,
            loader: "babel-loader"
          }
        ]
      }
    };
    
    

    src/index.ts

    import {hello} from "./utils";
    interface User {
        name: string;
        age: number;
    }
    const userToString = (user: User) => `${user.name}@${user.age}${hello()}`;
    export {userToString, User};
    

    src/utils.ts

    export const hello = () => {
      return 'hello';
    }
    

    完成上述package.json、webpack.config.js、src源代码三个部分,我们可以开始运行yarn build,但实际上会报错:

    ERROR in ./src/index.ts
    Module build failed (from ./node_modules/babel-loader/lib/index.js):
    SyntaxError: /Users/w4ngzhen/Projects/web-projects/webpack-babel-loader-demo/src/index.ts: Unexpected reserved word 'interface'. (1:0)
    
    > 1 | interface User {
        | ^
      2 |     name: string;
      3 |     age: number;
      4 | }
        at instantiate (/Users/w4ngzhen/Projects/web-projects/webpack-babel-loader-demo/node_modules/@babel/parser/lib/index.js:72:32)
    

    出现了语法的错误,报错的主要原因在于没有把整个babel处理ts的链路打通。目前的链路是:webpack找到入口ts文件,匹配上babel-loader,babel-loader交给@babel/core,@babel/core处理ts。由于我们没有给@babel/core配置plugin、preset,所以导致了babel还是以默认的js角度来处理ts代码,所以有语法报错。此时,我们需要添加.babelrc文件来指明让babel加载处理ts代码的插件:

    .babelrc

    {
      "presets": [
        "@babel/preset-env",
        "@babel/preset-typescript"
      ],
      "plugins": [
        "@babel/plugin-proposal-object-rest-spread",
        "@babel/plugin-proposal-class-properties"
      ]
    }
    

    完成配置以后,我们再次运行yarn build,编译通过,但是在dist下的index.js却是空白的!

    问题:babel-loader编译后,输出js内容空白

    如果按照上述的配置以后,我们能够成功编译但是却发现,输出的js代码是空白的!原因在于:我们编写的js代码,是按照类库的模式进行编写(在indexjs中只有导出一些函数却没有实际的使用),且webpack打包的时候,没有指定js代码的编译为什么样子的库。

    假如我们在index中编写一段具有副作用的代码:

    import {hello} from "./utils";
    interface User {
        name: string;
        age: number;
    }
    const userToString = (user: User) => `${user.name}@${user.age}${hello()}`;
    
    // 具备副作用:在id=app的元素上添加监听
    document
        .querySelector('#app')
        .addEventListener('click', () => {})
    
    export {userToString, User};
    

    此时我们使用生产模式(mode: 'production')来编译,会发现dist/index.js的内容如下:

    (() => {
      "use strict";
      document.querySelector("#app").addEventListener("click", (function () {
      }));
    })();
    

    会发现只有副作用代码,但是userToString相关的代码完全被剔除了!这时候,可能有读者会说,我导出的代码有可能别人会使用,你凭什么要帮我剔除?其实,因为webpack默认是生成项目使用的js,也就是做打包操作,他的目的是生成当前项目需要的js。在我们这个示例中,在没有写副作用之前,webpack认为打包是没有意义的,因为只有导出方法,却没有使用。那么,如果让webpack知道,我们需要做一个类库呢?在webpack中配置library字段即可:

    const {resolve} = require('path');
    module.exports = {
      entry: './src/index.ts',
      mode: 'production',
      output: {
        // ... ...
        library: { 
          // 配置library字段的相关配置,这里我们配置为commonjs2
          // 至于这块配置的意义,读者需要自行学习~
          type: 'commonjs2',
        },
      },
      // ... ...
    };
    
    

    tsc与babel编译的差异

    现在我们先编写一个简单错误代码

    interface User {
        name: string;
        age: number;
    }
    // user.myName并没有在User接口中提供
    const userToString = (user: User) => `${user.myName}@${user.age}`;
    export {userToString, User};
    

    在这个示例中,我们试图访问在User类型中不存在的myName字段。

    ts-loader

    前面我们提到了ts-loader内部调用的是tsc作为编译器,我们尝试运行基于ts-loader的webpack配置进行打包该模块,会发现报错:

    ... ...
          TS2551: Property 'myName' does not exist on type 'User'. Did you mean 'name'?
    ts-loader-default_e3b0c44298fc1c14
    
    webpack 5.74.0 compiled with 1 error in 2665 ms
    error Command failed with exit code 1.
    info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
    

    可以看得出来,tsc帮助我们提示了类型错误的地方,user这个类型并没有对应的myName字段。

    babel-loader

    我们切换一下到babel-loader对该ts文件进行编译,居然发现编译可以直接成功!并且,我们检查编译好的js代码,会发现这部分:

    // dist/index.js
    (() => {
      "use strict";
      // ... ...
      var r = function (e) {
        // 注意这个地方:依然在使用myName
        return "".concat(e.myName, "@").concat(e.age);
      };
      module.exports = o;
    })();
    
    

    编译好的js代码就在直接使用myName字段。为什么类型检查失效了?还记得我们前面提到的babel怎么处理ts的?

    Babel 如何处理 TypeScript 代码?它删除它

    是的,它删除了所有 TypeScript,将其转换为“常规的” JavaScript,并继续以它自己的方式愉快处理。

    是的,babel并没有进行类型检查,而是将各种类型移除掉以达到快速完成编译的目的。那么问题来了,我们如何让babel进行类型判断呢?实际上,我们没有办法让babel进行类型判断,必须要借助另外的工具进行。那为什么我们的IDE却能够现实ts代码的错误呢?因为IDE帮助我们进行了类型判断。

    主流IDE对TypeScript的类型检查

    不知道有没有细心的读者在使用IDEA的时候,发现一个ts项目的IDEA右下角展示了typescript:

    060-idea-ts-service

    VSCode也能看到类似:

    070-vscode-ts-service

    在同一台电脑上,甚至发现IDEA和VSCode的typescript版本都还不一样(4.7.4和4.7.3)。这是怎么一回事呢?实际上,IDE检测到你所在的项目是一个ts项目的时候(或包含ts文件),就会自动的启动一个ts的检测服务,专门用于所在项目的ts类型检测。这个ts类型检测服务,是通过每个IDE默认情况下自带的typescript中的tsc进行类型检测。

    但是,我们可以全局安装(npm -g)或者是为每个项目单独安装typescript,然后就可以让IDE选择启动独立安装的typescript。比如,我们在本项目中,安装一个特定版本的ts(版本4.7.2):

    yarn add -D typescript@4.7.2
    

    在IDEA中,设置 - Languages & Frameworks - TypeScript中,就可以选择IDEA启动的4.7.2版本的TypeScript为我们项目提供类型检查(注意看选项中有一个Bundled的TS,版本是4.7.4,就是默认的):

    080-idea-select-ts

    IDE之所以能够在对应的代码位置展示代码的类型错误,流程如下:

    090-ide-ts-service-flow

    但是,ts类型检查也要有一定的依据。譬如,有些类型定义的文件从哪里查找,是否允许较新的语法等,这些配置依然是由tsconfig.json来提供的,但若未提供,则IDE会使用一份默认的配置。如果要进行类型检测的自定义配置,则需要提供tsconfig.json。

    还记得我们前面的ts-loader吗?在代码编译期,ts-loader调用tsc,tsc读取项目目录下的tsconfig.json配置。而咱们编写代码的时候,又让IDE的ts读取该tsconfig.json配置文件进行类型检查。

    对于ts-loader项目体系来说,ts代码编译和ts的类型检测如下:

    100-ts-loader-and-ide

    然而,对于babel-loader项目体系就不像ts-loader那样了:

    110-babel-loader-and-ide

    在babel-loader体系中,代码的编译只取决于babel部分的处理,根类型没有根本的关系,而类型检查使用到的tsconfig和tsc则只作用在类型检查的部分,根ts代码编译没有任何关系。

  • 相关阅读:
    解决Win11/10中Edge浏览器页面加载不出来、打不开问题|有网但是打不开,加载不了
    IGBT和SiC MOSFET的驱动参数的计算方法
    (LinkedList与链表) 和 (ArrayList与顺序表)的区别
    vue中computed的使用场景举例
    基于Canal实现MySQL 8.0 数据库数据同步
    【C++11多线程编程】向线程函数传递参数
    SpEL表达式详解
    JDK8新特性:Stream流
    算法入门(栈-01)
    “混合”引擎为通用子模块提供动力,实现嵌入式I / O灵活性
  • 原文地址:https://www.cnblogs.com/w4ngzhen/p/16655833.html