因爲本次改造的項目爲一個經過NPM進行發佈的基礎服務包,所以本次採用TypeScript進行改造的目標是移除Babel全家桶,減少包體積,同時增長強類型約束從而避免從此開發時可能的問題。javascript
本次改造使用的是TypeScript v2.9.2,採用Webpack v4.16.0進行打包編譯。開發工具使用的是VSCode,使用中文語言包。預期目標是直接將TypeScript代碼經過loader直接編譯爲ES5的代碼。java
本文中涉及的問題有部分是TypeScript配置和使用的問題,也有部分是VSCode自己配置相關問題。node
在項目中,若是咱們使用了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
在JavaScript中,咱們常常會聲明一個空對象,而後再給這個屬性進行賦值。可是這個操做放在TypeScript中是會發生報錯的:windows
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.a = 1;
// 終端編譯報錯:TS2339: Property 'a' does not exist on type 'Window'.
// 編輯器報錯:[ts] 類型「Window」上不存在屬性「a」。
複製代碼
這也是由於TypeScript不容許增長沒有聲明的屬性致使的。
因爲咱們沒有辦法聲明windows屬性的值(或者說很困難),所以咱們須要經過下面這一種方式來解決:
(window as any).a = 1;
。這樣就可以保證編輯器和編譯時不會出錯。不過該方法只建議用於舊項目改造,咱們仍是要儘可能避免在window對象上面增長屬性,應該經過一個全局的數據管理器來進行數據存取。在項目中,使用到了一些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的代碼改形成爲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全家桶的支持。ts-map
和typescript-map
,這兩個包的查找效率都是o(n),低於原生類型的Map。所以推薦本身使用Object實現一個簡單的Map,具體實現方式能夠去網上找相關的Map原理分析與實踐(大體原理爲使用多個Object,存儲不一樣類型元素時使用不一樣容器,避免類型轉換問題)。將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"]
}
複製代碼
將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中,有多重不一樣的導出方式,不一樣的導出方式也對應着不一樣的引用方式。
目前我在項目改造中,遇到的模塊有這麼幾種方式:
而對於這幾種模塊,咱們也有不一樣的導入方式:
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文件和舊的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遷移時避免在我踩過的坑上再浪費時間。