概述
由於本次改造的專案為一個通過NPM進行釋出的基礎服務包,因此本次採用TypeScript進行改造的目標是移除Babel全家桶,減小包體積,同時增加強型別約束從而避免今後開發時可能的問題。
本次改造使用的是TypeScript v2.9.2,採用Webpack v4.16.0進行打包編譯。開發工具使用的是VSCode,使用中文語言包。預期目標是直接將TypeScript程式碼通過loader直接編譯為ES5的程式碼。
本文中涉及的問題有部分是TypeScript配置和使用的問題,也有部分是VSCode本身配置相關問題。
改造問題記錄與分析
VSCode相關
“無法找到相關模組”報錯
在專案中,如果我們使用了webpack.alias,可能會提示找不到模組。
具體錯誤如下:
終端編譯報錯:TS2307: Cannot find module '_utils/index'.編輯器報錯:[ts]找不到模組“_utils/index”。複製程式碼
這是由於編輯器無法讀取對應的別名資訊導致的。
此時我們需要檢查對應的模組是否存在。如果確認模組存在,且終端編譯編譯時不報錯,而只是編輯器報錯,則是因為編輯器無法讀取webpack配置,我們需要增加另外的配置。
解決方法:除了配置webpack.alias,還需要配置相對應的tsconfig.json
,具體配置如下所示:
"compilerOptions": {
"baseUrl": ".", "paths": {
"_util/*": [ "src/core/utils/*" ]
}
}複製程式碼
注:如果配置了tsconfig.json
以後還是報錯的話,需要重啟下VSCode,猜測是由於VSCode只在專案載入時讀取相關配置資訊。在JavaScript專案中的jsconfig.json
同理。
TypeScript相關
物件屬性賦值報錯
在JavaScript中,我們經常會宣告一個空物件,然後再給這個屬性進行賦值。但是這個操作放在TypeScript中是會發生報錯的:
let a = {
};
a.b = 1;
// 終端編譯報錯:TS2339: Property 'b' does not exist on type '{
}'.// 編輯器報錯:[ts] 型別“{
}”上不存在屬性“b”。複製程式碼
這是因為TypeScript不允許增加沒有宣告的屬性。
因此,我們有兩個辦法來解決這個報錯:
-
在物件中增加屬性定義(推薦)。具體方式為:
let a = {b: void 0
這個方法能夠從根本上解決當前問題,也能夠避免物件被隨意賦值的問題。
};
。 -
在物件中新增型別定義(推薦)。具體方式為如下:
interface obj {
[propName: string]: any
};
let a: obj = {
};
a.a = 1;
複製程式碼這樣也能夠避免報錯問題,並且不引入全物件any情況。
-
給
a
物件增加any屬性(應急)。具體方式為:let a: any = {
。這個方法能夠讓TypeScript型別檢查時忽略這個物件,從而編譯通過不報錯。這個方法適用於大量舊程式碼改造的情況。
};
Window物件屬性賦值報錯
與上一個情況類似,我們給一個物件中賦值一個不存在的屬性,會出現編輯器和編譯報錯:
window.a = 1;
// 終端編譯報錯:TS2339: Property 'a' does not exist on type 'Window'.// 編輯器報錯:[ts] 型別“Window”上不存在屬性“a”。複製程式碼
這也是因為TypeScript不允許增加沒有宣告的屬性導致的。
由於我們沒有辦法宣告windows屬性的值(或者說很困難),因此我們需要通過下面這一種方式來解決:
- 我們在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中新增的方法。
通過以上分析,我們可以使用如下方法解決:
-
可以使用lodash工具集中的相關方法,安裝時需要安裝
lodash.assign
和@types/lodash.assign
。並且lodash.assign
是一個CMD規範的包,需要通過import _assign = require('lodash.assing');
方式引入。 -
我們可以使用rest寫法,例如
let a = {...b
,也能夠達到一級淺拷貝的效果,具體效果如下:
};
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。
因此,我們解決這個問題的思路有三種:
- 將
tsconfig.json
配置中的target
屬性改為es6
,即輸出符合ES2015規範的程式碼。因為ES2015存在全域性的Promise物件,因此編譯和編輯器都不會報錯。該方法優點為配置簡單,無需改動程式碼,缺點為需要高階瀏覽器的支援或者Babel全家桶的支援。 - 捨棄Map型別,改用Object進行替代。這種改造比較費時費力,適用於工作量較小和不願意引入其他檔案的場景。
- 自行實現或者安裝一個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。
因此,我們解決這個問題的思路仍然有三種:
-
將
tsconfig.json
配置檔案配置中的target
屬性改為es6
,即輸出符合ES2015規範的程式碼。因為ES2015存在全域性的Promise物件,因此編譯和編輯器都不會報錯。該方法優點為配置簡單,無需改動程式碼,缺點為需要高階瀏覽器的支援或者Babel全家桶的支援。 -
引入一個Promise庫,如bluebird等比較知名的Promise庫。在安裝bluebird時需要同時安裝@types/bluebird宣告檔案。缺點就是引入的Promise庫較大,而且如果你的庫作為一個基礎庫時,可能會與其他的呼叫方的Promise庫產生衝突。
-
在
tsconfig.json
配置檔案中增加lib。此方法的原理是讓TypeScript編譯時引用外部的Promise物件,因此在編譯時不會報錯。此方式優點是不會引入任何其他程式碼,但是缺點是一定要保證在引用此庫的前提下,一定存在Promise物件。具體配置如下:"compilerOptions": {
"lib": ["es2015.promise"]
}複製程式碼
SetTimeout使用報錯
將ES2015程式碼改造成TypeScript程式碼時,如果使用了setTimeout和setInterval函式時,可能會出現無法找到該函式的報錯:
終端編譯報錯:TS2304: Cannot find name 'setTimeout'.編輯器報錯:[ts] 找不到名稱“setTimeout”。複製程式碼
這是由於編輯器和編譯時不知道當前程式碼執行環境導致的。
因此,我們解決這個問題的思路有兩種:
-
在
tsconfig.json
配置檔案中增加lib。讓TypeScript能夠知道當前的程式碼容器。具體示例如下:"compilerOptions": {
"lib": ["dom"]
}複製程式碼 -
安裝
@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] 應為宣告或語句。複製程式碼
這是由於兩者的模組語法不一樣導致的。
因此,我們解決這個問題只需要用下面這一種方法:
-
將上面的
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中,有多重不同的匯出方式,不同的匯出方式也對應著不同的引用方式。
目前我在專案改造中,遇到的模組有這麼幾種方式:
- CMD規範。
- ES2015 Module規範。
而對於這幾種模組,我們也有不同的匯入方式:
import _assign = require('lodash.assign');
//CMD規範import constant from './constant';
// ES2015 Module規範複製程式碼
如果你引入的檔案是一個非TypeScript而是JavaScript檔案時,你可能還需要增加宣告檔案。我們可以通過如下方法來新增宣告檔案:
-
增加@types檔案。這個方式針對於一些比較出名的類庫可以使用此方法。
-
在.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模組需要訪問defaultvar EventEmitter = require('eventemitter3');
var Session = require('./session.ts').default;
複製程式碼
這樣,我們就可以逐步的進行模組替換和改造,而不需要進行大規模的檔案替換和改名。
總結
在做專案TypeScript改造的過程中,遇到了不少大大小小的坑。很多問題在網上都沒有解決方案或者沒有說明白具體的解決步驟,因此希望通過這一篇文章來幫助大家在進行TypeScript遷移時避免在我踩過的坑上再浪費時間。