類型系統是 typescript 最吸引人的特性之一,但它的強大也讓咱們又愛又恨,每一個前端同窗在剛從 javascript 切換到 typescript 時都會有一段手足無措的時光,爲了避免讓編譯器報錯巴不得把全部變量都標註成 any 類型,然後在不斷地填坑中流下悔恨的淚水。謹以此文記錄我在學習 typescript 類型系統使用方法的過程當中遇到的一些問題,以供你們參考,避免大腦進水。javascript
(本文默認全部讀者熟知活動全部 ES6 語法及特性)前端
class
在 .d.ts 文件中定義時使用了 export default
語法的問題直接將類型附在變量聲明以後,並以冒號分隔。vue
const count: number = 3; const name: string = 'apples'; const appleCounts: string = count + name; assert.strictEqual(appleCounts, '3apples');
稍微複雜一點的,如數組 & 對象,按照其內成員結構進行聲明。java
const object: { a: number } = { a: 1, }; const array1: number[] = [1,]; const array2: Array<number> = [2,];
寫法極像箭頭函數。node
const someFn: (str: string) => number = function (str: string) { return +str; }
啥都能放,但並不建議大量使用,若是全部變量都是 any
,那 typescript 跟 javascript 又有什麼區別呢。typescript
const variable: any = '5';
空類型,通常用來表示函數沒有返回值。npm
function someFn (): void { 1 + 1; }
當一個變量多是多種類型時,使用 |
進行多個不一樣類型的分隔:數組
const nos: number | string = '1';
錯誤用法瀏覽器
必定,必定,必定 不要用 ,
做分隔符。app
const object: { a: [number, string] } = { a: 1, };
正確用法
const object: { a: number | string } = { a: 1, }; const array1: (number | string)[] = [ 1, ]; const array2: Array<number | string> = [ 2, ];
或
type numberOstring = number | string; const object: { a: numberOstring } = { a: 'yes', }; const array1: numberOstring[] = [ 'yes', ]; const array2: Array<numberOstring> = [ 'yes', ];
函數的入參類型跟聲明變量的類型時差很少,直接在變量名後跟類型名稱就能夠了。
返回值的類型則跟在參數的括號後面,冒號後面跟一個返回值的類型。
function someFn (arg1: string, arg2: number): boolean { return +arg1 > arg2; }
參數與返回值的聲明方法與普通函數無二。
setTimeout((): void => { console.log('six six six'); }, 50);
實例屬性記得必定要初始化。
class SomeClass { a: number = 1; b: string; static a: boolean; constructor () { this.b = 'str'; } method (str: string): number { return +str; } }
當你在聲明一個普通的對象(其餘類型也有可能,此處僅使用對象做爲例子)時,typescript 並不會自動爲你添加上對應的類型,這會形成你在賦值時觸發 TS2322: Type 'xxx' is not assignable to type 'xxx'.
錯誤,此時就須要使用顯式類型轉換來將兩邊的類型差別抹平。
類型轉換的前提是當前類型是真的能夠轉換爲目標類型的,任何須選屬性的缺失,或是根本沒法轉換的類型都是不容許的。
並不推薦這種方法,由於有時編輯器會把它當成 jsx 處理,產生沒必要要的 warning。
const object1: object = { a: 1, }; const object2: { a: number } = <{ a: number }>object1;
const object1: object = { a: 1, }; const object2: { a: number } = object1 as { a: number };
至關於聯結多個不一樣類型,併爲他們創造一個假名,平時寫多個類型並聯實在是太累了的時候能夠試試這個方法。
它的值能夠是類型,也能夠是具體的變量值。
type NumberOrString = number | string; type Direction = 'Up' | 'Right' | 'Down' | 'Left'; const num: NumberOrString = 1; const dir: Direction = 'Up';
注意:枚舉內部賦值時若是爲數字,能夠只賦值第一個,後面幾個會隨之遞增,若是爲字符串,則須要所有賦值,不然就會報錯。
enum Direction { Up = 1, Right, Down, Left } const dir: Direction = Direction.Up;
若是你的代碼中準備使用 enum 做爲右值,那請不要把 enum 聲明在 .d.ts 文件中,這是由於 ts 在編譯的時候 .d.ts 文件是做爲類型文件用的,並不會生成實體輸出,天然也就沒有地方會定義這個枚舉,這個時候用在代碼裏做爲右值的枚舉值就會由於找不到整個枚舉的定義,從而觸發 'xxx is undefined' 錯誤。
這也使得它在使用過程當中給咱們形成了各類各樣的麻煩(在 .d.ts 的 interface 聲明中使用 enum 真的是再正常不過的事情了),好比:
// my-types.d.ts declare const enum Direction { Up = 0, Right = 1, Down = 2, Left = 3, }
// usage.ts class SomeClass { dir = Direction.Up; }
編譯後的結果是:
// .d.ts 文件當場消失
// usage.js function SomeClass () { this.dir = Direction.up; }
瀏覽器在運行時根本找不到 Direction 定義的位置,天然就報 'Direction' is not defined
的錯了,但 type Direction = 'Up' | 'Right' | 'Down' | 'Left' 的方法就不會有這種問題,具體使用方式以下:
// my-types.d.ts type Direction = 'Up' | 'Right' | 'Down' | 'Left';
// usage.ts const dir: Direction = 'Up';
缺點是沒有類型提示,不能定義枚舉的內部值,判斷的時候也必須用對應的字符串進行字符串比對(汗。
聲明一種類型的對象,該類型的變量都必須知足該結構要求。
interface SomeInterface { str: string; num: number; } const object: SomeInterface = { str: 'str', num: 1, }; class SomeClass implements SomeInterface { num = 1; constructor () { this.str = 'str'; } }
同一個類能夠實現多個不一樣的接口,但前提是該類必定要實現每一個接口所要求的屬性。
interface Interface1 { str: string; } interface Interface2 { num: number; } class SomeClass implements Interface1, Interface2 { num = 1; constructor () { this.str = 'str'; } }
在多個不一樣文件,或是相同文件的不一樣位置聲明的同名接口,將會被合併成一個接口,名稱不變,成員變量取並集。
interface SomeInterface { str: string; } interface SomeInterface { num: number; } // 必須所有實現 const someInterface: SomeInterface = { str: 'str', num: 1, };
interface InterfaceFn { (str: string): boolean; } const fn1: InterfaceFn = (str: string): boolean => { return 10 < str.length; };
interface InterfaceFn { (str: string): boolean; standard: string; someFn(num: number): string; } // 必須進行顯式類型轉換 let fn1: InterfaceFn = function (str: string): boolean { return 10 < str.length; } as InterfaceFn; fn1.standard = 'str'; fn1.someFn = function (num: number): string { return `${num}`; };
接口能夠繼承類或是另外一個接口,與 ES6 繼承方法語法同樣,在此再也不贅述。
當該參數爲可選項時,能夠在名稱與類型表達式的冒號之間加一個問號 ?
用來表示該參數爲 __可選項__。
function someFn (arg1: number, arg2?: string): void {} someFn(1);
當該參數在不傳的時候有 缺省值 時,可使用 =
來爲其賦予 __缺省值__。
function someFn (arg1: number, arg2: number = 1): number { return arg1 + arg2; } someFn(1); // 2
可選項與 缺省值 能夠混搭。
function someFn (arg1: number, arg2: string = 'str', arg3?: string): void {} someFn(1);
但 可選項 參數後不可跟任何 非可選項 參數。(如下代碼當場爆炸)
function someFn (arg1: number, arg2?: string, arg3: string = 'str'): void {} someFn(1);
可選項與 缺省值 不可同時使用在同一個值上。(如下代碼當場爆炸)
function someFn (arg1: number, arg2?: string = 'str'): void {} someFn(1);
可選項 也可用在接口的聲明中(__缺省值__ 不行,由於接口是一種類型的聲明,並不是具體實例的實現)。
function someFn<T> (arg:T): T { return arg; } const str1: string = someFn<string>('str1'); const str2: string = someFn('str2');
function someFn<T, U> (arg1: T, arg2: U): T | U { return arg1; } const num1: string | number = someFn<string, number>('str1', 1); const str2: string | number = someFn('str2', 2);
const someFn: <T>(arg: T) => T = <T>(arg: T): T => { return arg; }; const str: string = someFn('str');
interface InterfaceFn { <T>(arg: T): T; } const someFn: InterfaceFn = <T>(arg: T): T => { return arg; }; const str: string = someFn('str');
const someFn: { <T>(arg: T): T; } = <T>(arg: T): T => { return arg; }; const str: string = someFn('str');
interface InterfaceFn<T> { (arg: T): T; } const someFn: InterfaceFn<string> = <T>(arg: T): T => { return arg; }; const str: string = someFn('str');
原理與 接口泛型
同樣。
class SomeClass<T> { someMethod (arg: T): T { return arg; } }
function someFn<T extends Date>(arg: T): number { return arg.getTime(); } const date = new Date();
keyof
、Record
、Pick
、Partial
太過複雜,真有需求還請自行查閱文檔。
在實際編碼過程當中,咱們常常會定義不少自定義的 接口 與 __類__,若是咱們在聲明變量的類型時須要用到它們,就算咱們的代碼中並無調用到它們的實例,咱們也必須手動引入它們(最多見的例子是各類包裝類,他們並不會處理參數中傳入的變量,但他們會在接口上強規範參數的類型,而且會將該變量透傳給被包裝的類的對應方法上)。
// foo.ts export default class FooClass { propA: number = 5; }
// bar.ts import FooClass from './foo.ts'; export class BarClass { foo?: FooClass; }
這種使用方法在調用數量較少時尚且能夠接受,但隨着業務的成長,被引用類型的數量和引用類型文件的數量同時上升,須要付出的精力便會隨其呈現出 o(n^2) 的增加趨勢。
這時咱們能夠選擇使用 .d.ts 文件,在你的業務目錄中建立 typings
文件夾,並在其內新建屬於你的 xxx.d.ts
文件,並在其中對引用較多的類和接口進行聲明(.d.ts 文件是用來進行接口聲明的,不要在 .d.ts 文件裏對聲明的結構進行實現)。
注意:.d.ts 文件只是用於分析類型聲明及傳參校驗,若是須要進行調用,還請直接 import 對應模塊。
// my-types.d.ts declare class FooClass { propA: number; methodA (arg1: string): void; }
foo.ts
文件略
// bar.ts // 不須要再 import 了 export default class BarClass { foo?: FooClass; }
其餘類型的聲明方式
// my-types.d.ts // 接口(沒有任何變化) interface InterfaceMy { propA: number; } // 函數 function myFn (arg1: string): number; // 類型 type myType = number | string;
declare
關鍵字,且不要使用 export
語句(若是使用了 export
,該文件就會變成實體 ts 文件,不會被 ts 的自動類型解析所識別,只能經過 import 使用)原本想寫一寫常見的 TS 編譯錯誤及形成這些錯誤的緣由來着,後來想了想,編譯出錯了都不會查,還寫什麼 TS 啊,
如下幾點是我在使用 typescript 類型系統過程當中遇到的一些智障問題與未解的疑問,歡迎你們一塊兒討論。
錯誤的爲 window 增添屬性的姿式:
window.someProperty = 1;
會觸發 TS2339: Property 'xxx' does not exist on type 'Window'
錯誤。
(window as any).someProperty = 1; (<any>window).someProperty = 1;
利用接口能夠多處聲明,由編譯器進行合併的特性進行 hack。
interface Window { someProperty: number; } window.someProperty = 1;
下面的代碼當場爆炸(由於 c: number,
最後的這個逗號)。
const someObject: { a: number, b: number, c: number, } = { a: 1, b: 2, c: 3, };
當一個函數在入參不一樣時有較大的行爲差距時,可使用函數重載梳理代碼結構。
注意:參數中有回調函數時,回調函數的參數數量變化並不該該致使外層函數使用重載,只應當在當前聲明函數的參數數量有變時才使用重載。
當同時聲明多個重載時,較爲準確的重載應該放在更前面。
重載的使用方法比較智障,須要先 聲明 這個函數的不一樣重載方式,而後緊接着再對這個函數進行定義。
定義時的參數個數取不一樣重載方法中參數個數最少的數量,隨後在其後追加 ...args: any[]
(或者更爲準確的類型定義寫法),用於接收多餘的參數。
定義的返回值爲全部重載返回值的並集。
然後在函數體內部實現時,經過判斷參數類型,自行實現功能的分流。
神奇的是 typescript 並不會校驗重載的實現是否會真的在調用某個重載時返回這個重載真正要求的類型的值,下方例子中即便不管觸發哪一個重載,都會返回 number
,也不會被 typescript 檢查出來。
猜測:屢次聲明一次實現難道是受制於 javascript 既有的語言書寫格式?
class SomeClass { someMethod (arg1: number, arg2: string, arg3: boolean): boolean; someMethod (arg1: number, arg2: string): string; someMethod (arg1: { arg1: number, arg2: string, }): number; someMethod (x: any, ...args: any[]): string | number | boolean { if ('object' === typeof x) { return 1; } else if (1 === args.length) { return 1; } else { return 1; } } }
function someFn (arg1: number, arg2: string, arg3: boolean): boolean; function someFn (arg1: number, arg2: string): string; function someFn (arg1: { arg1: number, arg2: string, }): number; function someFn (x: any, ...args: any[]): string | number | boolean { if ('object' === typeof x) { return 1; } else if (1 === args.length) { return 1; } else { return 1; } }
可使用 type
、interface
、class
對象 key
,可是使用方法十分麻煩,並且語法還不太同樣(type
使用 in
,interface
與 class
使用 :
)。
注意:索引值只可使用數字與字符串。
其實就是放開了限制,讓該類型的實例上能夠添加各類各樣的屬性。
這裏冒號 :
形式的不容許使用問號(可選項),但 in
形式的容許使用問號(可選項)。
但其實帶不帶結果都同樣,實例均可覺得空。
type SomeType1 = { [key: string]: string; } type SomeType2 = { [key in string]?: string; } const instance1: SomeType1 = {}; const instance2: SomeType2 = {};
這裏其中的 key
就成了必選項了,問號(可選項)也有效果了。
type audioTypes = 'ogg' | 'mp3' | 'wma'; type SomeType1 = { [key in audioTypes]: string; } type SomeType2 = { [key in audioTypes]?: string; } const instance5: SomeType1 = { 'ogg': 'ogg', 'mp3': 'mp3', 'wma': 'wma', }; const instance6: SomeType2 = {};
不能夠用問號。
interface SomeInterface { [key: string]: string; } const instance: SomeInterface = {};
只能經過 extends
已定義的 type
來實現。
type audioTypes = 'ogg' | 'mp3' | 'wma'; type SomeType = { [key in audioTypes]: string; } interface SomeInterface extends SomeType {} const instance: SomeInterface = { ogg: 'ogg', mp3: 'mp3', wma: 'wma', };
一樣也不可使用問號(可選值)。
class SomeClass { [key: string]: string; } const instance: SomeClass = new SomeClass();
經過 implements
其餘的 interface
、type
實現(多重實現能夠合併)。
請記得 interface
只是數據格式規範,implements
以後要記得在 class
裏寫實現
type audioTypes = 'ogg' | 'mp3' | 'wma'; type SomeType = { [key in audioTypes]: string; } interface SomeInterface { [key: string]: string; } class ClassExtended implements SomeInterface, SomeType { ogg = 'ogg'; mp3 = 'mp3'; wma = 'wma'; [key: string]: string; } const instance = new ClassExtended();
const someFn: (input: number, target: object) => SomeClass = (input: number, target: object): SomeClass => { // ... do sth };
咱們在平時使用一些類庫時,某一輩子態環境下的多個包,可能會依賴同一個基礎包。同一個生態環境下的包,更新節奏或快或慢,此時即可能會存在基礎包版本不一樣的問題,npm 的解決方案是多版本共存,每一個包引用本身對應版本的基礎包。由於 typescript 的類型是基於文件進行定義的,內部結構徹底相同的兩個同名類型,在不一樣的文件中聲明便成了不一樣的類型。
此處以 @forawesome
項目組下的 fontawesome
庫進行舉例,具體示例以下:
當咱們在 vue 中使用 fortawesome 時,須要把圖標文件從對應的包中導出(如免費基礎包:@fortawesome/free-solid-svg-icons
、免費公司 logo 包:@fortawesome/free-brands-svg-icons
),並使用 @fortawesome/fontawesome-svg-core
模塊的 library
方法導入到 vue 的運行環境中。
import { faVolumeUp, faPlay, faPause, } from '@fortawesome/free-solid-svg-icons'; import { faWeibo, faWeixin, } from '@fortawesome/free-brands-svg-icons'; library.add( faVolumeUp, faPlay, faPause, faWeibo, faWeixin );
但我再剛開始開發時只使用了基礎包,公司 logo 包是我在開發途中用到時才引入的,但這時 fortawesome 官方對整個庫進行了版本升級,具體功能並無什麼改變,只是 fix 了一些 bug,版本號也只升級了一個小版本。
但在編譯時 library.add 這裏報告了錯誤:
TS2345: Argument of type 'IconDefinition' is not assignable to parameter of type 'IconDefinitionOrPack'. Type 'IconDefinition' is not assignable to type 'IconPack'.`
通過跟進發現:
@forawesome/fontawesome-svg-core
的 library.add
的參數所要求的 IconDefinition
類型來自頂層 node_modules
安裝的公用的 @fortawesome/fontawesome-common-types
包的 index.d.ts
文件。
而 @fortawesome/free-brands-svg-icons
中字體的類型 IconDefinition
來自 @fortawesome/free-brands-svg-icons
自身內部 node_modules
裏安裝的高版本的 @fortawesome/fontawesome-common-types
的 index.d.ts
文件。
雖然兩個類型的定義如出一轍,但由於不是同一個文件定義的,因此是徹底不一樣的兩種類型,於是形成了類型不匹配,沒法正常編譯。
遇到這種問題時,升級對應包的版本就能夠了。
talk is cheap, show you the dunce.
節選自 vue/types/vue.d.ts
,我已經看暈了,調用方想要查錯的時候到底怎麼看呢。
export interface VueConstructor<V extends Vue = Vue> { new <Data = object, Methods = object, Computed = object, PropNames extends string = never>(options?: ThisTypedComponentOptionsWithArrayProps<V, Data, Methods, Computed, PropNames>): CombinedVueInstance<V, Data, Methods, Computed, Record<PropNames, any>>; // ideally, the return type should just contains Props, not Record<keyof Props, any>. But TS requires Base constructors must all have the same return type. new <Data = object, Methods = object, Computed = object, Props = object>(options?: ThisTypedComponentOptionsWithRecordProps<V, Data, Methods, Computed, Props>): CombinedVueInstance<V, Data, Methods, Computed, Record<keyof Props, any>>; new (options?: ComponentOptions<V>): CombinedVueInstance<V, object, object, object, Record<keyof object, any>>; extend<Data, Methods, Computed, PropNames extends string = never>(options?: ThisTypedComponentOptionsWithArrayProps<V, Data, Methods, Computed, PropNames>): ExtendedVue<V, Data, Methods, Computed, Record<PropNames, any>>; extend<Data, Methods, Computed, Props>(options?: ThisTypedComponentOptionsWithRecordProps<V, Data, Methods, Computed, Props>): ExtendedVue<V, Data, Methods, Computed, Props>; extend<PropNames extends string = never>(definition: FunctionalComponentOptions<Record<PropNames, any>, PropNames[]>): ExtendedVue<V, {}, {}, {}, Record<PropNames, any>>; extend<Props>(definition: FunctionalComponentOptions<Props, RecordPropsDefinition<Props>>): ExtendedVue<V, {}, {}, {}, Props>; extend(options?: ComponentOptions<V>): ExtendedVue<V, {}, {}, {}, {}>; nextTick(callback: () => void, context?: any[]): void; nextTick(): Promise<void> set<T>(object: object, key: string, value: T): T; set<T>(array: T[], key: number, value: T): T; delete(object: object, key: string): void; delete<T>(array: T[], key: number): void; directive( id: string, definition?: DirectiveOptions | DirectiveFunction ): DirectiveOptions; filter(id: string, definition?: Function): Function; component(id: string): VueConstructor; component<VC extends VueConstructor>(id: string, constructor: VC): VC; component<Data, Methods, Computed, Props>(id: string, definition: AsyncComponent<Data, Methods, Computed, Props>): ExtendedVue<V, Data, Methods, Computed, Props>; component<Data, Methods, Computed, PropNames extends string = never>(id: string, definition?: ThisTypedComponentOptionsWithArrayProps<V, Data, Methods, Computed, PropNames>): ExtendedVue<V, Data, Methods, Computed, Record<PropNames, any>>; component<Data, Methods, Computed, Props>(id: string, definition?: ThisTypedComponentOptionsWithRecordProps<V, Data, Methods, Computed, Props>): ExtendedVue<V, Data, Methods, Computed, Props>; component<PropNames extends string>(id: string, definition: FunctionalComponentOptions<Record<PropNames, any>, PropNames[]>): ExtendedVue<V, {}, {}, {}, Record<PropNames, any>>; component<Props>(id: string, definition: FunctionalComponentOptions<Props, RecordPropsDefinition<Props>>): ExtendedVue<V, {}, {}, {}, Props>; component(id: string, definition?: ComponentOptions<V>): ExtendedVue<V, {}, {}, {}, {}>; use<T>(plugin: PluginObject<T> | PluginFunction<T>, options?: T): void; use(plugin: PluginObject<any> | PluginFunction<any>, ...options: any[]): void; mixin(mixin: VueConstructor | ComponentOptions<Vue>): void; compile(template: string): { render(createElement: typeof Vue.prototype.$createElement): VNode; staticRenderFns: (() => VNode)[]; }; config: VueConfiguration; }
0 === san;