旧项目 TypeScript 改造问题与解决方案记

概述

因为本次改造的项目为一个经过NPM进行发布的基础服务包,所以本次采用TypeScript进行改造的目标是移除Babel全家桶,减少包体积,同时增长强类型约束从而避免从此开发时可能的问题。javascript

本次改造使用的是TypeScript v2.9.2,采用Webpack v4.16.0进行打包编译。开发工具使用的是VSCode,使用中文语言包。预期目标是直接将TypeScript代码经过loader直接编译为ES5的代码。java

本文中涉及的问题有部分是TypeScript配置和使用的问题,也有部分是VSCode自己配置相关问题。node

改造问题记录与分析

VSCode相关

“没法找到相关模块”报错

在项目中,若是咱们使用了webpack.alias,可能会提示找不到模块。webpack

具体错误以下:es6

终端编译报错:TS2307: Cannot find module '_utils/index'.
编辑器报错:[ts]找不到模块“_utils/index”。
复制代码

这是因为编辑器没法读取对应的别名信息致使的。web

此时咱们须要检查对应的模块是否存在。若是确认模块存在,且终端编译编译时不报错,而只是编辑器报错,则是由于编辑器没法读取webpack配置,咱们须要增长另外的配置。typescript

解决方法:除了配置webpack.alias,还须要配置相对应的tsconfig.json,具体配置以下所示:npm

"compilerOptions": {
    "baseUrl": ".",
    "paths": {
        "_util/*": [
            "src/core/utils/*"
        ]
    }
}
复制代码

注:若是配置了tsconfig.json之后仍是报错的话,须要重启下VSCode,猜想是因为VSCode只在项目加载时读取相关配置信息。在JavaScript项目中的jsconfig.json同理。json

TypeScript相关

对象属性赋值报错

在JavaScript中,咱们常常会声明一个空对象,而后再给这个属性进行赋值。可是这个操做放在TypeScript中是会发生报错的:windows

let a = {};

a.b = 1;
// 终端编译报错:TS2339: Property 'b' does not exist on type '{}'.
// 编辑器报错:[ts] 类型“{}”上不存在属性“b”。
复制代码

这是由于TypeScript不容许增长没有声明的属性。

所以,咱们有两个办法来解决这个报错:

  1. 在对象中增长属性定义(推荐)。具体方式为:let a = {b: void 0};。这个方法可以从根本上解决当前问题,也可以避免对象被随意赋值的问题。

  2. 在对象中添加类型定义(推荐)。具体方式为以下:

    interface obj {
        [propName: string]: any
    };
    let a: obj = {};
    
    a.a = 1;
    复制代码

    这样也可以避免报错问题,而且不引入全对象any状况。

  3. a对象增长any属性(应急)。具体方式为:let a: any = {};。这个方法可以让TypeScript类型检查时忽略这个对象,从而编译经过不报错。这个方法适用于大量旧代码改造的状况。

Window对象属性赋值报错

与上一个状况相似,咱们给一个对象中赋值一个不存在的属性,会出现编辑器和编译报错:

window.a = 1;
// 终端编译报错:TS2339: Property 'a' does not exist on type 'Window'.
// 编辑器报错:[ts] 类型“Window”上不存在属性“a”。
复制代码

这也是由于TypeScript不容许增长没有声明的属性致使的。

因为咱们没有办法声明windows属性的值(或者说很困难),所以咱们须要经过下面这一种方式来解决:

  1. 咱们在windows使用时增长一个类型转换,即(window as any).a = 1;。这样就可以保证编辑器和编译时不会出错。不过该方法只建议用于旧项目改造,咱们仍是要尽可能避免在window对象上面增长属性,应该经过一个全局的数据管理器来进行数据存取。

ES2015 Object新增的原型链上的方法报错

在项目中,使用到了一些Object原型链上面的一些ES2015新增的方法,如Object.assignObject.values等,此时编译会失败,同时VSCode会提示报错:

终端编译报错:TS2339: Property 'assign' does not exist on type 'ObjectConstructor'.
编辑器报错:[ts] 类型“ObjectConstructor”上不存在属性“assign”。
复制代码

这是因为咱们在tsconfig.json中指定的target是ES5,而TypeScript并无相关的polyfill,所以咱们没法使用ES2015中新增的方法。

经过以上分析,咱们可使用以下方法解决:

  1. 可使用lodash工具集中的相关方法,安装时须要安装lodash.assign@types/lodash.assign。而且lodash.assign是一个CMD规范的包,须要经过import _assign = require('lodash.assing');方式引入。

  2. 咱们可使用rest写法,例如let a = {...b};,也可以达到一级浅拷贝的效果,具体效果以下:

    image.png

ES2015新增的数据结构Map初始化报错

将ES2015的代码改形成为TypeScript代码时,若是你使用了ES2015新增的Map类型,那在编辑器仍是终端编译中编译时都会报错:

终端编译报错:TS2693: 'Map' only refers to a type, but is being used as a value here.
编辑器报错报错:[ts] “Map”仅表示类型,但在此处却做为值使用。
复制代码

这是因为TypeScript并无提供相关的数据类型,也没有对应的polyfill。

所以,咱们解决这个问题的思路有三种:

  1. tsconfig.json配置中的target属性改成es6,即输出符合ES2015规范的代码。由于ES2015存在全局的Promise对象,所以编译和编辑器都不会报错。该方法优势为配置简单,无需改动代码,缺点为须要高级浏览器的支持或者Babel全家桶的支持。
  2. 舍弃Map类型,改用Object进行替代。这种改造比较费时费力,适用于工做量较小和不肯意引入其余文件的场景。
  3. 自行实现或者安装一个Map包。这种方法改形成本较小,缺点就是会引入额外的代码或者包,而且代码效率没法保证。例如ts-maptypescript-map,这两个包的查找效率都是o(n),低于原生类型的Map。所以推荐本身使用Object实现一个简单的Map,具体实现方式能够去网上找相关的Map原理分析与实践(大体原理为使用多个Object,存储不一样类型元素时使用不一样容器,避免类型转换问题)。

ES2015新增的Promise使用报错

将ES2015的代码改形成为TypeScript代码时,若是你使用了ES2015的新增的Promise类型,那在编辑器仍是终端编译编译时都会报错:

终端编译报错: TS2693: 'Promise' only refers to a type, but is being used as a value here.
编辑器报错:[ts] “Promise”仅表示类型,但在此处却做为值使用。
复制代码

这是因为TypeScript并无提供Promise数据类型,也没有对应的polyfill。

所以,咱们解决这个问题的思路仍然有三种:

  1. tsconfig.json配置文件配置中的target属性改成es6,即输出符合ES2015规范的代码。由于ES2015存在全局的Promise对象,所以编译和编辑器都不会报错。该方法优势为配置简单,无需改动代码,缺点为须要高级浏览器的支持或者Babel全家桶的支持。

  2. 引入一个Promise库,如bluebird等比较知名的Promise库。在安装bluebird时须要同时安装@types/bluebird声明文件。缺点就是引入的Promise库较大,并且若是你的库做为一个基础库时,可能会与其余的调用方的Promise库产生冲突。

  3. tsconfig.json配置文件中增长lib。此方法的原理是让TypeScript编译时引用外部的Promise对象,所以在编译时不会报错。此方式优势是不会引入任何其余代码,可是缺点是必定要保证在引用此库的前提下,必定存在Promise对象。具体配置以下:

    "compilerOptions": {
    	"lib": ["es2015.promise"]
    }
    复制代码

SetTimeout使用报错

将ES2015代码改形成TypeScript代码时,若是使用了setTimeout和setInterval函数时,可能会出现没法找到该函数的报错:

终端编译报错:TS2304: Cannot find name 'setTimeout'.
编辑器报错:[ts] 找不到名称“setTimeout”。
复制代码

这是因为编辑器和编译时不知道当前代码运行环境致使的。

所以,咱们解决这个问题的思路有两种:

  1. tsconfig.json配置文件中增长lib。让TypeScript可以知道当前的代码容器。具体示例以下:

    "compilerOptions": {
    	"lib": ["dom"]
    }
    复制代码
  2. 安装@types/node。该方法适用于node环境下或者采用webpack打包时能够引入node代码。该方法直接经过npm install @types/node便可安装完成,解决报错问题。

模块引用和导出报错

在ES2015的代码中,咱们能够经过@babel/plugin-proposal-export-default-from插件来直接导出引入的文件,具体示例以下:

export Session from './session'; // 报错
export * from '_models/read-item'; // 不报错
复制代码

而在TypeScript中,这种写法是会报错的:

终端编译报错:TS1128: Declaration or statement expected.
编辑器报错:[ts] 应为声明或语句。
复制代码

这是因为二者的模块语法不同致使的。

所以,咱们解决这个问题只须要用下面这一种方法:

  1. 将上面的export from的语法稍加调整来适配TypeScript语法。具体改造以下:

    export {default as Session} from '_models/session'; //调整后不报错
    export * from '_models/read-item';// 以前不报错不须要调整
    复制代码

泛型定义

咱们在项目中常常会遇到这种状况,咱们须要保证传入的属性类型的同时,还须要保证其与某个函数的参数一致,如:

interface props {
    value: number | string, 
    onChange: (v: string | number) => void // 参数类型值须要与value一致
}
复制代码

为了解决这个问题,咱们须要用到泛型定义:

interface Props<T extends string | number> {
    value: T,
    onChange: (v: T) => void
}
复制代码

此时,当value的类型肯定时,参数的类型也就变得和value同样肯定了。

模块引用

当咱们使用TypeScript时,常常会出现引用其余模块甚至是JavaScript其余包的状况。在TypeScript中,有多重不一样的导出方式,不一样的导出方式也对应着不一样的引用方式。

目前我在项目改造中,遇到的模块有这么几种方式:

  1. CMD规范。
  2. ES2015 Module规范。

而对于这几种模块,咱们也有不一样的导入方式:

import _assign = require('lodash.assign'); //CMD规范
import constant from './constant'; // ES2015 Module规范
复制代码

若是你引入的文件是一个非TypeScript而是JavaScript文件时,你可能还须要增长声明文件。咱们能够经过以下方法来添加声明文件:

  1. 增长@types文件。这个方式针对于一些比较出名的类库可使用此方法。

  2. 在.d.ts文件中增长声明,这个声明全局有效。具体方式以下:

    declare module 'promiz';
    复制代码

    对于JSON文件,你也须要采用这种声明方式,具体方式以下:

    declare module "*.json" {
        const value: any;
        export const version: string;
        export default value;
    }
    复制代码

经过以上方法,咱们就能够应对不一样模块的规范和不一样类型的文件。

TypeScript局部替换

在进行重构改造的时候,咱们在最开始可能只能逐个模块进行替换。咱们须要新的TypeScript文件和旧的JavaScript文件可以和平共存进行编译运行。

针对这种需求,咱们只须要在webpack编译的loader中增长相关ts文件的配置,而且在extension中增长.ts后缀的支持。相关配置以下:

{
    module: {
        rules: [
            {
                test: /ts$/,
                use: [{
                    loader: 'ts-loader',
                    options: {
                        silent: process.env.env === 'production' ? true : false
                    }
                }]
            }
        ]
    },
    extensions: ['.ts', '.js']
}
复制代码

而后,咱们只须要在JavaScript中文件引入时,带上.ts后缀便可,以下例所示:

// 本人以前使用的是CMD规范,所以引入ES2015模块须要访问default
var EventEmitter = require('eventemitter3');
var Session = require('./session.ts').default;
复制代码

这样,咱们就能够逐步的进行模块替换和改造,而不须要进行大规模的文件替换和更名。

总结

在作项目TypeScript改造的过程当中,遇到了很多大大小小的坑。不少问题在网上都没有解决方案或者没有说明白具体的解决步骤,所以但愿经过这一篇文章来帮助你们在进行TypeScript迁移时避免在我踩过的坑上再浪费时间。

相关文章
相关标签/搜索