巧用 TypeScript (一)

以下問題來自于與公司小伙伴以及網(wǎng)友的討論,整理成章,希望提供另一種思路(避免踩坑)解決問題。

函數(shù)重載

TypeScript 提供函數(shù)重載的功能,用來處理因函數(shù)參數(shù)不同而返回類型不同的使用場景,使用時,只需為同一個函數(shù)定義多個類型即可,簡單使用如下所示:

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;

它也適用于參數(shù)不同,返回值類型相同的場景,我們只需要知道在哪種函數(shù)類型定義下能使用哪些參數(shù)即可。

考慮如下例子:

interface User {
  name: string;
  age: number;
}

declare function test(para: User | number, flag?: boolean): number;

在這個 test 函數(shù)里,我們的本意可能是當(dāng)傳入?yún)?shù) paraUser 時,不傳 flag,當(dāng)傳入 paranumber 時,傳入 flag。TypeScript 并不知道這些,當(dāng)你傳入 paraUser 時,flag 同樣允許你傳入:

const user = {
  name: 'Jack',
  age: 666
}

// 沒有報(bào)錯,但是與想法違背
const res = test(user, false);

使用函數(shù)重載能幫助我們實(shí)現(xiàn):

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: 參數(shù)不匹配
const res = test(user, false);

實(shí)際項(xiàng)目中,你可能要多寫幾步,如在 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 {
    // 具體實(shí)現(xiàn)
    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 版本推出映射類型以來,它便不斷被完善與增強(qiáng)。在 2.1 版本中,可以通過 keyof 拿到對象 key 類型, 內(nèi)置 Partial、ReadonlyRecord、Pick 映射類型;2.3 版本增加 ThisType ;2.8 版本增加 ExcludeExtract、NonNullableReturnType、InstanceType;同時在此版本中增加條件類型與增強(qiáng) keyof 的能力;3.1 版本支持對元組與數(shù)組的映射。這些無不意味著映射類型在 TypeScript 有著舉足輕重的地位。

其中 ThisType 并沒有出現(xiàn)在官方文檔中,它主要用來在對象字面量中鍵入 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 的出現(xiàn),Vue 2.5 才得以增強(qiáng)對 TypeScript 的支持。

雖已內(nèi)置了很多映射類型,但在很多時候,我們需要根據(jù)自己的項(xiàng)目自定義映射類型:

比如你可能想取出接口類型中的函數(shù)類型:

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 里調(diào)用 this.find() 時,實(shí)際上是調(diào)用 this.value.find(),但是此時 TypeScript 并不知道這些:

class SomeClass {
  value = [1, 2, 3];

  someMethod() {
    this.value.find(/* ... */);  // ok
    this.find(/* ... */);        // Error:SomeClass 沒有 find 方法。
  }
}

借助于映射類型,和 interface + class 的聲明方式,可以實(shí)現(xiàn)我們的目的:

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

導(dǎo)出 SomeClass 類時,也能使用。

可能有點(diǎn)不足的地方,在這段代碼里 interface SomeClass extends SelectArrayMethod<number> {} 你需要手動添加范型的具體類型(暫時沒想到更好方式)。

類型斷言

類型斷言用來明確的告訴 TypeScript 值的詳細(xì)類型,合理使用能減少我們的工作量。

比如一個變量并沒有初始值,但是我們知道它的類型信息(它可能是從后端返回)有什么辦法既能正確推導(dǎo)類型信息,又能正常運(yùn)行了?有一種網(wǎng)上的推薦方式是設(shè)置初始值,然后使用 typeof 拿到類型(可能會給其他地方用)。然而我可能比較懶,不喜歡設(shè)置初始值,這時候使用類型斷言可以解決這類問題:

interface User {
  name: string;
  age: number;
}

export default class NewRoom extends Vue {
  private user = {} as User;
}

在設(shè)置初始化時,添加斷言,我們就無須添加初始值,編輯器也能正常的給予代碼提示了。如果 user 屬性很多,這樣就能解決大量不必要的工作了,定義的 interface 也能給其他地方使用。

枚舉類型

枚舉類型分為數(shù)字類型與字符串類型,其中數(shù)字類型的枚舉可以當(dāng)標(biāo)志使用:

// 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 源碼里,定義了大量如上所示的基于數(shù)字類型的常量枚舉。它們是一種有效存儲和表示布爾值集合的方法

《深入理解 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

上例代碼中 |= 用來添加一個標(biāo)志,&=~ 用來刪除標(biāo)志,| 用來合并標(biāo)志。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 這是16年5月份編輯的一份比較雜亂適合自己觀看的學(xué)習(xí)記錄文檔,今天18年5月份再次想寫文章,發(fā)現(xiàn)簡書還為我保存起的...
    Jenaral閱讀 3,131評論 2 9
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,544評論 19 139
  • 學(xué)習(xí)筆記:“孩子們是最好的觀察者,他們無時無刻不在觀察你的一言一行,他們透過觀察你、模仿你、慢慢成為你、最后才有可...
    英語老師陸玉閱讀 214評論 0 0
  • 一撙清酒一明月, 一人獨(dú)醉一星河。 一時隱就一世蹙, 一籌疏影一愁離。 一面春風(fēng)一舒展, 一絲紅暈一似夢。 一迭轉(zhuǎn)...
    不記名大叔閱讀 155評論 0 1
  • 今天自己一直在苦思冥想這個PPT 怎么講,發(fā)現(xiàn)自己有點(diǎn)過度緊張了,過分的在意自己能給多少了,其實(shí)我只是一個引導(dǎo),所...
    Hi_張閱讀 155評論 0 0

友情鏈接更多精彩內(nèi)容