怎麼上... 咳咳,你們別想歪,這是一篇純技♂術文章。javascript
什麼?尤大要把Vue 3.0所有改爲用Typescript來寫?這不是逗我嗎,那我是否是要用TypeScript來寫Vue應用了? html
好吧,Vue3.0可能最快也要19年年底纔出來,Vue3.0是會對Ts使用者更友好,而不是隻能用ts了,尤大使用ts的緣由也是由於ts的靜態類型檢測以及ts的表現比flow愈來愈好了。自從巨硬大步邁向開源,前端圈子多了不少新工具好比VS Code、TypeScript。我的認爲TypeScript真正火起來仍是由於前端應用的複雜度不斷飆升,這帶來的問題就是維護性以及擴展性會變差。尤爲在編寫類庫的時候,更是須要考慮各個類以及方法的複用性和擴展性,因此會使用到設計模式來優化代碼。還有更重要的就是,編碼效率的提升,靜態系統無疑是下降了調試bug的時間。前端
優勢java
缺點node
let foo = 123; foo = '456'; // Error: cannot assign `string` to `number
Webpack已經發布到版本4.41了,相信不少小夥伴已經上了webpack4了,Webpack4對typescript的支持也是8錯的,它最大的變化莫過於"零配置"以及將commonChunks plugin插件嵌入爲webpack內置。最新版本:
webpack
npm install -g typescript
假設咱們有一個用TypeScript編寫的Student類。git
class Student { private name: string; constructor(name: string) { this.name = name; } }
使用typescript compiler來編譯它程序員
tsc student.ts
編譯後的結果是根據編譯選項來生成的標準JavaScript文件。es6
var Student = /** @class */ (function () { function Student(name) { this.name = name; } return Student; }());
TypeScript-->ES Next的Javascript版本-->兼容性較好的JavaScript。
以前已經安裝了TypeScript compiler,一般會在compiler option中指定typescript是要編譯到支持ES5/ES6/ES Next的JavaScript版本,可是在實踐中咱們還須要利用Babel這個結果再進行一次轉譯,這麼作的緣由有兩個。github
const path = require('path') const webpack = require('webpack') const config = { entry: './src/index.ts', module: { rules: [ { // ts-loader: convert typescript to javascript(esnext), // babel-loader: converts javascript(esnext) to javascript(backward compatibility) test: /\.(tsx|ts)?$/, use: ['babel-loader', 'ts-loader'], exclude: /node_modules/ }, ] }, resolve: { extensions: ['.tsx', '.ts', '.js'], alias: { '@': path.resolve(__dirname, './src'), 'mobx': path.resolve(__dirname, './node_modules/mobx/lib/mobx.es6.js') } }, }
簡單介紹一下typescript的編譯選項,一般會在這裏指定編譯目標JS版本,代碼的模塊化方式以及代碼的檢查規則等。
allowJS
表示是否容許編譯JavaScript文件。target
表示ECMAScript目標版本,好比‘ESNext’、'ES2015'。module
表示模塊化的方式,好比'commonjs'、'umd'或'es2105'(es module)moduleResolution
表示的是模塊解析的策略,即告訴編譯器在哪裏找到當前模塊,指定爲'node'時,就採用nodejs的模塊解析策略,完整算法能夠在Node.js module documentation找到;當它的值指定爲'classic'時則採用TypeScript默認的解析策略,這種策略主要是爲了兼容舊版本的typescript。strict
是否啓動全部的嚴格類型檢查選型,包括'noImplicitAny','noImplicitThis'等。lib
表示編譯過程當中須要引入的庫文件的列表,根據實際應用場景來引入。experimentalDecorators
是爲了支持裝飾器語法的選項,由於在項目中使用了Mobx作狀態管理,因此須要啓用裝飾器語法。include
選項表示編譯的目錄outDir
表示編譯結果輸出的目錄。{ "compileOnSave": true, "compilerOptions": { "target": "esnext", "module": "esnext", "moduleResolution": "node", "sourceMap": true, "strict": true, "allowJs": true, "experimentalDecorators": true, "outDir": "./dist/", "lib": [ "es2015", "dom", "es2016", "es2017", "dom.iterable", "scripthost", "webworker" ] }, "include": [ "src/**/*.ts" ] }
tslint是針對typescript的lint工具,相似eslint遵循Airbnb Style或Standard Style,eslint也能夠指定要遵循的typescript規範,目前在tslint官方,給出了三種內置預設,recommended
、latest
以及all
,省去了咱們去對tslint每條規則進行配置的麻煩。
recommended
是穩定版的規則集,通常的typescript項目中使用它比較好,遵循SemVer。latest
會不斷更新以包含每一個TSLint版本中最新規則的配置,一旦TSLint發佈了break change,這個配置也會跟隨着一塊兒更新。all
將全部規則配置爲最爲嚴格的配置。tslint規則
tslint的規則是有嚴重性等級的劃分,每條規則能夠配置default
error
warning
或off
。tslint預設提供了不少在代碼實踐中提煉出來的規則,我認爲有下面若干的規則,咱們會常常遇到,或者須要關注一下。
only-arrow-functions
只容許使用箭頭函數,不容許傳統的函數表達式。promise-function-async
任何返回promise的函數或方法,都應該使用'async'標識出來;await-promise
在'await'關鍵字後面跟隨的值不是promise時會警告,規範咱們異步代碼的編寫。no-console
禁止在代碼中使用'console'方法,便於去除無用的調試代碼。no-debugger
禁止在代碼中使用'debugger'方法,同上。no-shadowed-variable
當在局部做用域和外層做用域存在同名的變量時,稱爲shadowing,這會致使局部做用域會沒法訪問外層做用域中的同名變量。no-unused-variable
不容許存在,未使用的變量、import或函數等。這個規則的意義在於避免編譯錯誤,同時由於聲明瞭變量卻不適用,也致使了讀者混淆。max-line-length
要求每行的字數有限制;quotemark
指定對字符串常量,使用的符號,通常指定'single';這個看團隊風格了。prefer-const
儘量用'const'聲明變量,而不是'let',不會被重複賦值的變量,默認使用'const';其餘規則你們能夠詳細看tslint官方文檔,使用lint能夠更好地規範代碼風格,保持團隊代碼風格的統一,避免容易致使編譯錯誤的問題以及提升可讀性和維護性。
tslint的特殊flags
咱們用ts寫代碼的時候,常常會遇到一行代碼的字數過長的狀況,此時可使用tslint提供的flag來使得該行不受規則的約束。
// tslint:disable-next-line:max-line-length private paintPopupWithFade<T extends THREE.Object3D>(paintObj: T, popupStyleoption: PopupStyleOption, userDataType: number) { //... }
實際上,tslint提示是該行的字數違反了 max-line-length規則,此處能夠經過增長註釋 // tslint: disable-next-line: rulex
來禁用這個規則。
"鴨子"類型??(黑人問號), 第一次看到這名詞我也很懵逼, 其實它說的是結構型類型,而目前類型檢測主要分爲結構型(structural)類型以及名義型(nominal)類型。
interface Point2D { x: number; y: number; } interface Point3D { x: number; y: number; z: number; } var point2D: Point2D = { x:0, y: 10} var point3D: Point3D = { x: 0, y: 10, z: 20} function iTakePoint2D(point: Point2D) { /*do sth*/ } iTakePoint2D(point2D); // 類型匹配 iTakePoint2D(point3D); // 類型兼容,結構類型 iTakePoint2D({ x:0 }); // 錯誤: missing information `y`
區別
知道了typescript是個'鴨子類型'後,咱們就會想到一個問題,ts這種鴨子類型
怎麼判斷類型啊,好比下面這個例子:
public convertString2Image(customizeData: UserDataType) { if (Helper.isUserData(customizeData)) { const errorIcon = searchImageByName(this.iconImage, statusIconKey); if (errorIcon) { (customizeData as UserData).title.icon = errorIcon; } } else if (Helper.isUserFloorData(customizeData)) { // do nothing } else { // UserAlertData let targetImg; const titleIcon = (customizeData as UserAlertData)!.title.icon; if (targetImg) { (customizeData as UserAlertData).title.icon = targetImg; } } return customizeData; }
該方法是根據傳入的用戶數據來將傳入的icon字段用實際對應的圖片填充,customizeData
是用戶數據,此時咱們須要根據不一樣類型來調用searchImageByName
方法去加載對應的圖片,因此咱們此時須要經過一些類型判斷的方法在運行時判斷出該對象的類型。
基礎的類型判斷
基本的類型判斷方法咱們可能會想到typeof
和instanceof
,在ts中,其實也可使用這兩個操做符來判斷類型,好比:
typeof
判斷類型function doSomething(x: number | string) { if(typeof x === 'string') { console.log(x.toFixed()); // Property 'toFixed' does not exist on type 'string' console.log(x.substr(1)); } else if (typeof x === 'number') { console.log(x.toFixed()); console.log(x.substr(1)); // Property 'substr' does not exist on type 'number'. } }
能夠看到使用typeof
在運行時判斷基礎數據類型是可行的,能夠在不一樣的條件塊中針對不一樣的類型執行不一樣的業務邏輯,可是對於Class
或者Interface
定義的非基礎類型,就必須考慮其餘方式了。
instanceof
判斷類型下面這個例子根據傳入的geo
對象的類型執行不一樣的處理邏輯:
public addTo(geo: IMap | IArea | Marker) { this.gisObj = geo; this.container = this.draw()!; if (!this.container) { return; } this.mapContainer.appendChild<HTMLDivElement>(this.container!); if (this.gisObj instanceof IMap) { this.handleDuration(); } else if(this.gisObj instanceof Marker) { // } }
能夠看到,使用instanceof
動態地判斷類型是可行的,並且類型能夠是Class
關鍵字聲明的類型,這些類型都擁有複雜的結構,並且擁有構造函數。總地來講,使用instanceof
判斷類型的兩個條件是:
prototype
屬性類型不能爲any
。利用類型謂詞來判斷類型
結合一開始的例子,咱們要去判斷一個鴨子類型,在ts中,咱們有特殊的方式,就是類型謂詞
(type predicate)的概念,這是typescript的類型保護機制,它會在運行時檢查確保在特定做用域內的類型。針對那些Interface
定義的類型以及映射出來的類型,並且它並不具備構造函數,因此咱們須要本身去定義該類型的檢查方法,一般也被稱爲類型保護
。
例子中的調用的兩個基於類型保護的方法的實現
public static isUserData(userData: UserDataType): userData is UserData { return ((userData as UserData).title !== undefined) && ((userData as UserData).subTitle !== undefined) && ((userData as UserData).body !== undefined) && ((userData as UserData).type === USER_DATA_TYPE.USER_DATA); } public static isUserFloorData(userFloorData: UserDataType): userFloorData is UserFloorData { return ((userFloorData as UserFloorData).deviceAllNum !== undefined) && ((userFloorData as UserFloorData).deviceNormalNum !== undefined) && ((userFloorData as UserFloorData).deviceFaultNum !== undefined) && ((userFloorData as UserFloorData).deviceOfflineNum !== undefined); }
實際上,咱們要去判斷這個類型的結構,這也是爲何ts的類型系統被稱爲鴨子類型
,咱們須要遍歷對象的每個屬性來區分類型。換句話說,若是定義了兩個結構徹底相同的類型,即使類型名不一樣也會判斷爲相同的類型~
索引類型(index types),使用索引類型,編譯器就可以檢查使用了動態屬性名的代碼。ts中經過索引訪問操做符keyof
獲取類型中的屬性名,好比下面的例子:
function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] { return names.map(n => o[n]); } interface Person { name: string; age: number; } let person: Person { name: 'Jarid', age: 35 } let strings: string[] = pluck(person, ['name']);
原理
編譯器會檢查name
是否真的爲person
的一個屬性,而後keyof T
,索引類型查詢操做符,對於任何類型T, keyof T
的結果爲T上已知的屬性名的聯合。
let personProps: keyof Person; // 'name' | 'age'
也就是說,屬性名也能夠是任意的interface類型!
索引訪問操做符T[K]
索引類型指的其實ts中的屬性能夠是動態類型,在運行時求值時才知道類型。你能夠在普通的上下文中使用T[K]
類型,只須要確保K extends keyof T
便可,例以下面:
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] { return o[name]; }
原理:o:T
和 name:K
表示o[name]: T[K]
當你返回T[K]
的結果,編譯器會實例化key的真實類型,所以getProperty的返回值的類型會隨着你須要的屬性改變而改變。
let name: string = getProperty(person, 'name'); let age: number = getProperty(person, 'age'); let unknown = getProperty(person, 'unknown'); // error, 'unknown' is not in 'name' | 'age'
索引類型和字符串索引簽名keyof
和 T[k]
與字符串索引簽名進行交互。
好比:
interface Map<T> { [key: string]: T; // 這是一個帶有字符串索引簽名的類型, keyof T 是 string } let keys: keyof Map<number>; // string let value: Map<number>['foo']; // number
Map<T>
是一個帶有字符串索引簽名的類型,那麼keyof T 會是string。
背景
在使用typescript時,會有一個問題咱們是繞不開的 --> 如何從舊的類型中建立新類型即映射類型。
interface PersonPartial { name?: string; age?: number; } interface PersonReadonly { readonly name: string; readonly age: number; }
能夠看到PersonReadOnly
這個類型僅僅是對PersonParial
類型的字段只讀化設置,想象一下 若是這個類型是10個字段那就須要重複寫這10個字段。咱們有沒辦法不去重複寫這種樣板代碼,而是經過映射獲得新類型? 答案就是映射類型,
映射類型的原理
新類型以相同的形式去轉換舊類型裏每一個屬性:
type Readonly<T> { readonly [P in keyof T]: T[P]; }
它的語法相似於索引簽名的語法,有三個步驟:
Keys
,包含了要迭代的屬性名的集合好比下面這個例子
type Keys = 'option1' | 'option2'; type Flags = { [K in keys]: boolean };
Keys
,是硬編碼的一串屬性名,而後這個屬性的類型是boolean,所以這個映射類型等同於:
type Flags = { option1: boolean; option2: boolean; }
典型用法
咱們常常會遇到的或者更通用的是(泛型的寫法):
type Nullable<T> = { [P in keyof T]: T[P] | null }
聲明一個Person類型,一旦用Nullable類型轉換後,獲得的新類型的每個屬性就是容許爲null的類型了。
// test interface Person { name: string; age: number; greatOrNot: boolean; } type NullPerson = Nullable<Person>; const nullPerson: NullPerson = { name: '123', age: null, greatOrNot: true, };
騷操做
利用類型映射,咱們能夠作到對類型的Pick
和Omit
,Pick
是ts自帶的類型,好比下面的例子:
export interface Product { id: string; name: string; price: string; description: string; author: string; authorLink: string; } export type ProductPhotoProps = Pick<Product, 'id' | 'author'| 'authorlink' | 'price'>; // Omit的實現 export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; export type ProductPhotoOtherProps = Omit<Product, 'name' | 'description'>;
咱們能夠把已有的Product
類型中的若干類型pick
出來組成一個新類型;也能夠把若干的類型忽略掉,把剩餘的屬性組成新的類型。
好處
never
首先,never
類型有兩種場景:
// 返回never的函數必須存在沒法達到的終點 function error(message: string): never { throw new Error(message); } // 推斷的返回值類型爲never function fail() { return error("Something failed"); }
voidvoid
也有它的應用場景
void
。void
類型或者返回值標記爲void
能夠提升代碼的可讀性,讓人明確該方法是不會有返回值,寫測試時也能夠避免去關注返回值。public remove(): void { if (this.container) { this.mapContainer.removeChild(this.container); } this.container = null; }
小結
never
實質表示的是那些永遠不存在值的類型,也能夠表示函數表達式或箭頭函數表達式的返回值。void
類型,變量仍然能夠被賦值undefined
或null
,可是never
是隻能被返回值爲never
的函數賦值。ts中用enum
關鍵字來定義枚舉類型,彷佛在不少強類型語言中都有枚舉的存在,然而Javascrip沒有,枚舉能夠幫助咱們更好地用有意義的命名去取代那些代碼中常常出現的magic number
或有特定意義的值。這裏有個在咱們的業務裏用到的枚舉類型:
export enum GEO_LEVEL { NATION = 1, PROVINCE = 2, CITY = 3, DISTRICT = 4, BUILDING = 6, FLOOR = 7, ROOM = 8, POINT = 9, }
由於值都是number
,通常也被稱爲數值型枚舉。
基於數值的枚舉
ts的枚舉都是基於數值類型的,數值能夠被賦值到枚舉好比:
enum Color { Red, Green, Blue } var col = Color.Red; col = 0; // 與Color.Red的效果同樣
ts內部實現
咱們看看上面的枚舉值爲數值類型的枚舉類型會怎樣被轉爲JavaScript:
// 轉譯後的Javascript define(["require", "exports"], function (require, exports) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var GEO_LEVEL; (function (GEO_LEVEL) { GEO_LEVEL[GEO_LEVEL["NATION"] = 1] = "NATION"; GEO_LEVEL[GEO_LEVEL["PROVINCE"] = 2] = "PROVINCE"; GEO_LEVEL[GEO_LEVEL["CITY"] = 3] = "CITY"; GEO_LEVEL[GEO_LEVEL["DISTRICT"] = 4] = "DISTRICT"; GEO_LEVEL[GEO_LEVEL["BUILDING"] = 6] = "BUILDING"; GEO_LEVEL[GEO_LEVEL["FLOOR"] = 7] = "FLOOR"; GEO_LEVEL[GEO_LEVEL["ROOM"] = 8] = "ROOM"; GEO_LEVEL[GEO_LEVEL["POINT"] = 9] = "POINT"; })(GEO_LEVEL = exports.GEO_LEVEL || (exports.GEO_LEVEL = {})); });
很是有趣,咱們先不去想爲何要這麼轉譯,換個角度思考,其實上面的代碼說明了這樣一個事情:
console.log(GEO_LEVEL[1]); // 'NATION' console.log(GEO_LEVEL['NATION']) // 1 // GEO_LEVEL[GEO_LEVEL.NATION] === GEO_LEVEL[1]
因此其實咱們能夠經過這個枚舉變量GEO_LEVEL去將下標表示的枚舉轉爲key
表示的枚舉,key
表示的枚舉也能夠轉爲用下標表示。