以下問題來自於與公司小夥伴以及網友的討論,整理成章,希望提供另一種思路(避免踩坑)解決問題。
函式過載
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 版本增加 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
原始碼裡,定義了大量如上所示的基於數字型別的常量列舉。它們是一種有效儲存和表示布林值集合的方法。
在 《深入理解 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
複製程式碼
上例程式碼中 |=
用來新增一個標誌,&=
和 ~
用來刪除標誌,|
用來合併標誌。