巧用 TypeScript(四)
用 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
複製程式碼