舊項目 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遷移時避免在我踩過的坑上再浪費時間。

相關文章
相關標籤/搜索