巧用 TypeScript (一)
以下問題來自於與公司小夥伴以及網友的討論,整理成章,希望提供另一種思路(避免踩坑)解決問題。
函式過載
TypeScript 提供函式過載的功能,用來處理因函式引數不同而返回型別不同的使用場景,使用時,只需為同一個函式定義多個型別即可,簡單使用如下所示:
declare function test(a: number): number; declare function test(a: string): string; const resS = test('Hello World');// resS 被推斷出型別為 string; const resN = test(1234);// resN 被推斷出型別為 number; 複製程式碼
它也適用於引數不同,返回值型別相同的場景,我們只需要知道在哪種函式型別定義下能使用哪些引數即可。
考慮如下例子:
interface User { name: string; age: number; } declare function test(para: User | number, flag?: boolean): number; 複製程式碼
在這個test
函式裡,我們的本意可能是當傳入引數para
是User
時,不傳flag
,當傳入para
是number
時,傳入flag
。TypeScript 並不知道這些,當你傳入para
為User
時,flag
同樣允許你傳入:
const user = { name: 'Jack', age: 666 } // 沒有報錯,但是與想法違背 const res = test(user, false); 複製程式碼
使用函式過載能幫助我們實現:
interface User { name: string; age: number; } declare function test(para: User): number; declare function test(para: number, flag: boolean): number; const user = { name: 'Jack', age: 666 }; // bingo // Error: 引數不匹配 const res = test(user, false); 複製程式碼
實際專案中,你可能要多寫幾步,如在class
中:
interface User { name: string; age: number; } const user = { name: 'Jack', age: 123 }; class SomeClass { /** * 註釋 1 */ public test(para: User): number; /** * 註釋 2 */ public test(para: number, flag: boolean): number; public test(para: User | number, flag?: boolean): number { // 具體實現 return 11; } } const someClass = new SomeClass(); // ok someClass.test(user); someClass.test(123, false); // Error someClass.test(123); someClass.test(user, false); 複製程式碼
對映型別
自從 TypeScript 2.1 版本推出對映型別以來,它便不斷被完善與增強。在 2.1 版本中,可以通過keyof
拿到物件key
型別, 內建Partial
、Readonly
、Record
、Pick
對映型別;2.3 版本增加ofollow,noindex">
ThisType
;2.8 版本增加Exclude
、Extract
、NonNullable
、ReturnType
、InstanceType
;同時在此版本中增加條件型別與增強keyof
的能力;3.1 版本支援對元組與陣列的對映。這些無不意味著對映型別在 TypeScript 有著舉足輕重的地位。
其中
ThisType
並沒有出現在官方文件中,它主要用來在物件字面量中鍵入this
:
// Compile with --noImplicitThis type ObjectDescriptor<D, M> = { data?: D; methods?: M & ThisType<D & M>;// Type of 'this' in methods is D & M } function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M { let data: object = desc.data || {}; let methods: object = desc.methods || {}; return { ...data, ...methods } as D & M; } let obj = makeObject({ data: { x: 0, y: 0 }, methods: { moveBy(dx: number, dy: number) { this.x += dx;// Strongly typed this this.y += dy;// Strongly typed this } } }); obj.x = 10; obj.y = 20; obj.moveBy(5, 5); 複製程式碼
正是由於ThisType
的出現,Vue 2.5 才得以增強對 TypeScript 的支援。
雖已內建了很多對映型別,但在很多時候,我們需要根據自己的專案自定義對映型別:
比如你可能想取出介面型別中的函式型別:
type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T]; type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>; interface Part { id: number; name: string; subparts: Part[]; updatePart(newName: string): void; } type T40 = FunctionPropertyNames<Part>;// "updatePart" type T42 = FunctionProperties<Part>;// { updatePart(newName: string): void } 複製程式碼
比如你可能為了便捷,把本屬於某個屬性下的方法,通過一些方式 alias 到其他地方。
舉個例子:SomeClass
下有個屬性value = [1, 2, 3]
,你可能在 Decorators 給類添加了此種功能:在SomeClass
裡呼叫this.find()
時,實際上是呼叫this.value.find()
,但是此時 TypeScript 並不知道這些:
class SomeClass { value = [1, 2, 3]; someMethod() { this.value.find(/* ... */);// ok this.find(/* ... */);// Error:SomeClass 沒有 find 方法。 } } 複製程式碼
藉助於對映型別,和interface + class
的宣告方式,可以實現我們的目的:
type ArrayMethodName = 'filter' | 'forEach' | 'find'; type SelectArrayMethod<T> = { [K in ArrayMethodName]: Array<T>[K] } interface SomeClass extends SelectArrayMethod<number> {} class SomeClass { value = [1, 2, 3]; someMethod() { this.forEach(/* ... */)// ok this.find(/* ... */)// ok this.filter(/* ... */)// ok this.value// ok this.someMethod()// ok } } const someClass = new SomeClass(); someClass.forEach(/* ... */)// ok someClass.find(/* ... */)// ok someClass.filter(/* ... */)// ok someClass.value// ok someClass.someMethod()// ok 複製程式碼
匯出SomeClass
類時,也能使用。
可能有點不足的地方,在這段程式碼裡interface SomeClass extends SelectArrayMethod<number> {}
你需要手動新增範型的具體型別(暫時沒想到更好方式)。
型別斷言
型別斷言用來明確的告訴 TypeScript 值的詳細型別,合理使用能減少我們的工作量。
比如一個變數並沒有初始值,但是我們知道它的型別資訊(它可能是從後端返回)有什麼辦法既能正確推導型別資訊,又能正常運行了?有一種網上的推薦方式是設定初始值,然後使用typeof
拿到型別(可能會給其他地方用)。然而我可能比較懶,不喜歡設定初始值,這時候使用型別斷言可以解決這類問題:
interface User { name: string; age: number; } export default class NewRoom extends Vue { private user = {} as User; } 複製程式碼
在設定初始化時,新增斷言,我們就無須新增初始值,編輯器也能正常的給予程式碼提示了。如果user
屬性很多,這樣就能解決大量不必要的工作了,定義的interface
也能給其他地方使用。
列舉型別
列舉型別分為數字型別與字串型別,其中數字型別的列舉可以當標誌使用:
// https://github.com/Microsoft/TypeScript/blob/master/src/compiler/types.ts#L3859 export const enum ObjectFlags { Class= 1 << 0,// Class Interface= 1 << 1,// Interface Reference= 1 << 2,// Generic type reference Tuple= 1 << 3,// Synthesized generic tuple type Anonymous= 1 << 4,// Anonymous Mapped= 1 << 5,// Mapped Instantiated= 1 << 6,// Instantiated anonymous or mapped type ObjectLiteral= 1 << 7,// Originates in an object literal EvolvingArray= 1 << 8,// Evolving array type ObjectLiteralPatternWithComputedProperties = 1 << 9,// Object literal pattern with computed properties ContainsSpread= 1 << 10, // Object literal contains spread operation ReverseMapped= 1 << 11, // Object contains a property from a reverse-mapped type JsxAttributes= 1 << 12, // Jsx attributes type MarkerType= 1 << 13, // Marker type used for variance probing JSLiteral= 1 << 14, // Object type declared in JS - disables errors on read/write of nonexisting members ClassOrInterface = Class | Interface } 複製程式碼
在 TypeScript
src/compiler/types
原始碼裡,定義了大量如上所示的基於數字型別的常量列舉。它們是一種有效儲存和表示布林值集合的方法
。
在pings%2Fenums.html%23%25E4%25BD%25BF%25E7%2594%25A8%25E6%2595%25B0%25E5%25AD%2597%25E7%25B1%25BB%25E5%259E%258B%25E4%25BD%259C%25E4%25B8%25BA%25E6%25A0%2587%25E5%25BF%2597" rel="nofollow,noindex">《深入理解 TypeScript》 中有一個使用例子:
enum AnimalFlags { None= 0, HasClaws= 1 << 0, CanFly= 1 << 1, HasClawsOrCanFly = HasClaws | CanFly } interface Animal { flags: AnimalFlags; [key: string]: any; } function printAnimalAbilities(animal: Animal) { var animalFlags = animal.flags; if (animalFlags & AnimalFlags.HasClaws) { console.log('animal has claws'); } if (animalFlags & AnimalFlags.CanFly) { console.log('animal can fly'); } if (animalFlags == AnimalFlags.None) { console.log('nothing'); } } var animal = { flags: AnimalFlags.None }; printAnimalAbilities(animal); // nothing animal.flags |= AnimalFlags.HasClaws; printAnimalAbilities(animal); // animal has claws animal.flags &= ~AnimalFlags.HasClaws; printAnimalAbilities(animal); // nothing animal.flags |= AnimalFlags.HasClaws | AnimalFlags.CanFly; printAnimalAbilities(animal); // animal has claws, animal can fly 複製程式碼
上例程式碼中|=
用來新增一個標誌,&=
和~
用來刪除標誌,|
用來合併標誌。