因爲本次改造的項目爲一個經過NPM進行發佈的基礎服務包,所以本次採用TypeScript進行改造的目標是移除Babel全家桶,減少包體積,同時增長強類型約束從而避免從此開發時可能的問題。node
本次改造使用的是TypeScript v2.9.2,採用Webpack v4.16.0進行打包編譯。開發工具使用的是VSCode,使用中文語言包。預期目標是直接將TypeScript代碼經過loader直接編譯爲ES5的代碼。webpack
本文中涉及的問題有部分是TypeScript配置和使用的問題,也有部分是VSCode自己配置相關問題。es6
在項目中,若是咱們使用了webpack.alias,可能會提示找不到模塊。web
具體錯誤以下:typescript
終端編譯報錯:TS2307: Cannot find module '_utils/index'. 編輯器報錯:[ts]找不到模塊「_utils/index」。
這是因爲編輯器沒法讀取對應的別名信息致使的。npm
此時咱們須要檢查對應的模塊是否存在。若是確認模塊存在,且終端編譯編譯時不報錯,而只是編輯器報錯,則是由於編輯器沒法讀取webpack配置,咱們須要增長另外的配置。json
解決方法:除了配置webpack.alias,還須要配置相對應的tsconfig.json
,具體配置以下所示:windows
"compilerOptions": { "baseUrl": ".", "paths": { "_util/*": [ "src/core/utils/*" ] } }
注:若是配置了tsconfig.json
之後仍是報錯的話,須要重啓下VSCode,猜想是因爲VSCode只在項目加載時讀取相關配置信息。在JavaScript項目中的jsconfig.json
同理。promise
在JavaScript中,咱們常常會聲明一個空對象,而後再給這個屬性進行賦值。可是這個操做放在TypeScript中是會發生報錯的:瀏覽器
let a = {}; a.b = 1; // 終端編譯報錯:TS2339: Property 'b' does not exist on type '{}'. // 編輯器報錯:[ts] 類型「{}」上不存在屬性「b」。
這是由於TypeScript不容許增長沒有聲明的屬性。
所以,咱們有兩個辦法來解決這個報錯:
let a = {b: void 0};。
這個方法可以從根本上解決當前問題,也可以避免對象被隨意賦值的問題。在對象中添加類型定義(推薦)。具體方式爲以下:
[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.assign`和`Object.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](https://user-gold-cdn.xitu.io/2018/7/24/164cb3c419a9f512?w=245&h=152&f=png&s=11291) ### 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-map`和`typescript-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;
這樣,咱們就能夠逐步的進行模塊替換和改造,而不須要進行大規模的文件替換和更名。 # 總結