巧用 TypeScript(四)

三毛丶發表於2018-12-16

用 Decorator 限制型別

Decorator 可用於限制類方法的返回型別,如下所示:

const TestDecorator = () => {
  return (
    target: Object,
    key: string | symbol,
    descriptor: TypedPropertyDescriptor<() => number>   // 函式返回值必須是 number
  ) => {
    // 其他程式碼
  }
}

class Test {
  @TestDecorator()
  testMethod() {
    return '123';   // Error: Type 'string' is not assignable to type 'number'
  }
}
複製程式碼

你也可以用泛型讓 TestDecorator 的傳入引數型別與 testMethod 的返回引數型別相容:

const TestDecorator = <T>(para: T) => {
  return (
    target: Object,
    key: string | symbol,
    descriptor: TypedPropertyDescriptor<() => T>
  ) => {
    // 其他程式碼
  }
}

class Test {
  @TestDecorator('hello')
  testMethod() {
    return 123;      // Error: Type 'number' is not assignable to type 'string'
  }
}
複製程式碼

泛型的型別推斷

在定義泛型後,有兩種方式使用,一種是傳入泛型型別,另一種使用型別推斷,即編譯器根據其他引數型別來推斷泛型型別。簡單示例如下:

declare function fn<T>(arg: T): T;      // 定義一個泛型函式

const fn1 = fn<string>('hello');        // 第一種方式,傳入泛型型別 string
const fn2 = fn(1);                      // 第二種方式,從引數 arg 傳入的型別 number,來推斷出泛型 T 的型別是 number
複製程式碼

它通常與對映型別一起使用,用來實現一些比較複雜的功能。

Vue Type 簡單實現

如下一個例子:

type Options<T> = {
  [P in keyof T]: T[P];
}

declare function test<T>(o: Options<T>): T;

test({ name: 'Hello' }).name     // string
複製程式碼

test 函式將傳入引數的所有屬性取出來,現在我們來一步一步加工,實現想要的功能。

首先,更改傳入引數的形式,由 { name: 'Hello' } 的形式變更為 { data: { name: 'Hello' } },呼叫函式的返回值型別不變,即 test({ data: { name: 'Hello' } }).name 的值也是 string 型別。

這並不複雜,這隻需要把傳入引數的 data 型別設定為 T 即可:

declare function test<T>(o: { data: Options<T> }): T;

test({data: { name: 'Hello' }}).name     // string
複製程式碼

data 物件裡,含有函式時,它也能運作:

const param = {
  data: {
    name: 'Hello',
    someMethod() {
      return 'hello world'
    }
  }
}

test(param).someMethod()    // string
複製程式碼

接著,考慮一種特殊的函式情景,像 Vue 中 Computed 一樣,不呼叫函式,也能取出函式的返回值型別。現在傳入引數的形式變更為:

const param = {
  data: {
    name: 'Hello'
  },
  computed: {
    age() {
      return 20;
    }
  }
}
複製程式碼

一個函式的型別可以簡單的看成是 () => T 的形式,物件中的方法型別,可以看成 a: () => T 的形式,在反向推導時(由函式返回值,來推斷型別 a 的型別),可以利用它,現在,需要新增一個對映型別 Computed<T>,用來處理 computed 裡的函式:

type Options<T> = {
  [P in keyof T]: T[P]
}

type Computed<T> = {
  [P in keyof T]: () => T[P]
}

interface Params<T, M> {
  data: Options<T>;
  computed: Computed<M>;
}

declare function test<T, M>(o: Params<T, M>): T & M;

const param = {
  data: {
    name: 'Hello'
  },
  computed: {
    age() {
      return 20
    }
  }
}

test(param).name    // string
test(param).age     // number
複製程式碼

最後,結合巧用 TypeScript(一) 中提到的 ThisType 對映型別,可以輕鬆的實現在 computed age 方法下訪問 data 中的資料:

type Options<T> = {
  [P in keyof T]: T[P]
}

type Computed<T> = {
  [P in keyof T]: () => T[P]
}

interface Params<T, M> {
  data: Options<T>;
  computed: Computed<M>;
}

declare function test<T, M>(o: Params<T, M> & ThisType<T & M>): T & M;

test({
  data: {
    name: 'Hello'
  },
  computed: {
    age() {
      this.name;    // string
      return 20;
    }
  }
})
複製程式碼

至此,只有 data, computed 簡單版的 Vue Type 已經實現。

扁平陣列構建樹形結構

扁平陣列構建樹形結構即是將一組扁平陣列,根據 parent_id(或者是其他)轉換成樹形結構:

// 轉換前資料
const arr = [
  { id: 1, parentId: 0, name: 'test1'},
  { id: 2, parentId: 1, name: 'test2'},
  { id: 3, parentId: 0, name: 'test3'}
];


// 轉化後
[
  {
    id: 1,
    parentId: 0,
    name: 'test1',
    children: [
      { id: 2, parentId: 1, name: 'test2', children: [] }
    ]
  },
  {
    id: 3,
    parentId: 0,
    name: 'test3',
    children: []
  }
]
複製程式碼

如果 children 欄位名字不變,函式的型別並不難寫,它大概是如下樣子:

interface Item {
  id: number;
  parentId: number;
  name: string;
}

type TreeItem = Item & { children: TreeItem[] | [] };

declare function listToTree(list: Item[]): TreeItem[];

listToTree(arr).forEach(i => i.children)    // ok
複製程式碼

但是在很多時候,children 欄位的名字並不固定,而是從引數中傳進來:

const options = {
  childrenKey: 'childrenList'
}

listToTree(arr, options);
複製程式碼

此時,children 欄位名稱,應該為 childrenList

[
  {
    id: 1,
    parentId: 0,
    name: 'test1',
    childrenList: [
      { id: 2, parentId: 1, name: 'test2', childrenList: [] }
    ]
  },
  {
    id: 3,
    parentId: 0,
    name: 'test3',
    childrenList: []
  }
]
複製程式碼

實現的思路大致是前文所說的利用泛型的型別推斷,從傳入的 options 引數中,得到 childrenKey 的型別,然後再傳給 TreeItem,如下:

interface Options<T extends string> {   // 限制為 string 型別
  childrenKey: T;
}

declare function listToTree<T extends string = 'children'>(list: Item[], options: Options<T>): TreeItem<T>[];
複製程式碼

當 options 為 { childrenKey: 'childrenList' } 時,T 能被正確推匯出為 childrenList。接著,只需要在 TreeItem 中,把 children 修改為傳入的 T 即可:

interface Item {
  id: number;
  parentId: number;
  name: string;
}

interface Options<T extends string> {
  childrenKey: T;
}

type TreeItem<T extends string> = Item & { [key in T]: TreeItem<T>[] | [] };

declare function listToTree<T extends string = 'children'>(list: Item[], options: Options<T>): TreeItem<T>[];

listToTree(arr, { childrenKey: 'childrenList' }).forEach(i => i.childrenList)    // ok
複製程式碼

有一點侷限性,由於物件字面量的 Fresh 的影響,當 options 不是以物件字面量的形式傳入時,需要給它斷言:

const options = {
  childrenKey: 'childrenList' as 'childrenList'
}

listToTree(arr, options).forEach(i => i.childrenList)    // ok
複製程式碼

更多

相關文章