如何在打字稿中获取递归对象的类型作为函数参数?
How to get the type of a recursive object as function argument in typescript?
我有一个有两个参数的函数
1:对象数组和
2:一个字符串数组,是对象之前的键
并根据它们创建分组。如何在以下递归函数中键入结果对象值 any
?
import { groupBy, transform } from 'lodash'
const multiGroupBy = (array: objectType[], [group, ...restGroups]: string[]) => {
if (!group) {
return array
}
const currGrouping = groupBy(array, group)
if (!restGroups.length) {
return currGrouping
}
return transform(
currGrouping,
(result: { [key: string]: any }, value, key) => {
// What do I type as the any value above
result[key] = multiGroupBy(value, [...restGroups])
},
{}
)
}
const orderedGroupRows = multiGroupBy(arrayOfObjects, ['category', 'subCategory'])
所以结果将是这样的递归结构,具体取决于字符串数组的长度
const result = {
"Category Heading 1": {
"SubCategory Heading 1": [
...arrayofOfObjects
]
},
"Category Heading 2": {
"SubCategory Heading 2": [
...arrayofOfObjects
]
}
}
首先,重要的是确定 input/output 类型的 multiGroupBy()
关系。存在一系列可能的关系;在这个范围的一端,你有简单的类型,对你没有多大好处,比如无论你将什么传递到 multiGroupBy()
,输出的类型都是 object
。另一方面,你有非常复杂的类型,它可能代表输出的确切值,这取决于输入的内容......想象一下你调用 multiGroupBy([{x: "a", y: "b"}, {x: "a", y: "c"}], ["x", "y"])
的情况,并且 return 值有类型 {a: {b: [{x: "a", y: "b"}], c: [{x: "a", y: "c"}]}}
。这种类型可能很容易使用,但代价是很难实现,而且可能很脆弱。
所以我们需要在这些目的之间取得平衡。看起来有用的最简单的键入如下所示:
type MultiGroupBy<T> = (T[]) | { [k: string]: MultiGroupBy<T> };
declare const multiGroupBy: <O extends object>(
array: O[],
groups: (keyof O)[]
) => MultiGroupBy<O>;
如果您输入 O[]
类型的 array
和 Array<keyof O>
类型的 groups
,则输出为 MultiGroupBy<O>
类型。在这里,我们将输出表示为输入类型数组的 union 或数组或字典的类似字典的对象。该定义是无限递归的,并且无法指定对象的深度。为了使用它,你必须测试每个级别。
此外,当输出是一个字典时,编译器不知道这个字典的键是什么。这是一个限制,并且有解决它的方法,但这会使事情变得非常复杂,并且由于您已经接受了未知密钥,所以我不会深入讨论它。
所以让我们探索一种跟踪输出结构有多深的类型:
type MultiGroupBy<T, K extends any[] = any[]> =
number extends K['length'] ? (T[] | { [k: string]: MultiGroupBy<T, K> }) :
K extends [infer F, ...infer R] ? { [k: string]: MultiGroupBy<T, R> } :
T[];
declare const multiGroupBy: <O extends object, K extends Array<keyof O>>(
array: O[],
groups: [...K]
) => MultiGroupBy<O, K>;
现在 multiGroupBy()
接受类型 O[]
的 array
和类型 K
的 groups
,其中 K
是 constrained to be assignable to Array<keyof O>
. And the output type, MultiGroupBy<T, K>
, uses conditional types 来确定输出类型。简要地说:
如果K
是一个未知长度的数组,那么编译器会输出一些与MultiGroupBy<T>
的旧定义非常相似的东西,数组的并集或嵌套字典;如果您在编译时不知道数组长度,那确实是您能做的最好的事情。否则,编译器tries to see if the K
is a tuple认为可以拆分成它的第一个元素F
和其余元素R
的一个元组。如果可以,那么输出类型是一个字典,它的值是 MultiGroupBy<T, R>
类型……这是一个递归步骤,每递归一次,元组就会短一个元素。另一方面,如果编译器无法将 K
拆分为先行后行,则它为空...在这种情况下,输出类型为 T[]
数组。
所以打字看起来非常接近我们想要的。
不过,我们还没有完全完成。上面的类型允许 groups
中的键是 array
元素中的 any 键,包括那些 属性 值不是字符串的键:
const newArray = [{ foo: { a: 123 }, bar: 'hey' }];
const errors = multiGroupBy(newArray, ['foo']); // accepted?!
你不想允许。所以我们必须让 multiGroupBy()
的输入稍微复杂一点:
type KeysOfPropsWithStringValues<T> =
keyof T extends infer K ? K extends keyof T ?
T[K] extends string ? K : never
: never : never;
declare const multiGroupBy:
<O extends object, K extends KeysOfPropsWithStringValues<O>[]>(
array: O[], groups: [...K]) => MultiGroupBy<O, K>;
类型 KeysOfPropsWithStringValues<T>
使用条件类型查找 T
的所有键 K
,其中 T[K]
可分配给 string
。它是 keyof T
的子类型。你可以用其他方式来写,比如 中的 KeysMatching
,但它是一样的。
然后我们将 K
约束为 Array<KeysOfPropsWithStringValues<O>>
而不是 Array<keyof O>
。现在可以使用:
const errors = multiGroupBy(newArray, ['foo']); // error!
// ----------------------------------> ~~~~~
// Type '"foo"' is not assignable to type '"bar"'.
为了确保我们对这些类型感到满意,让我们看看编译器如何看待示例用法:
interface ObjectType {
category: string;
subCategory: string;
item: string;
}
declare const arrayOfObjects: ObjectType[];
const orderedGroupRows = multiGroupBy(arrayOfObjects,
['category', 'subCategory' ]);
/* const orderedGroupRows: {
[k: string]: {
[k: string]: ObjectType[];
};
} */
看起来不错!编译器将 orderedGroupRows
视为 ObjectType
.
数组字典的字典
最后,实施。事实证明,在不使用 type assertions or any
. See microsoft/TypeScript#33912 之类的东西以获取更多信息的情况下,实现通用函数 returning 条件类型几乎是不可能的。因此,这是我在不重构您的代码的情况下所能做的最好的事情(即使我重构了它也不会变得更好):
const multiGroupBy = <
O extends object,
K extends Array<KeysOfPropsWithStringValues<O>>
>(
array: O[],
[group, ...restGroups]: [...K]
): MultiGroupBy<O, K> => {
if (!group) {
return array as MultiGroupBy<O, K>; // assert
}
const currGrouping = groupBy(array, group);
if (!restGroups.length) {
return currGrouping as MultiGroupBy<O, K>; // assert
}
return transform(
currGrouping,
(result, value, key) => {
result[key] = multiGroupBy(value, [...restGroups]);
},
{} as any // give up and use any
);
};
如果我们使用原始的联合类型,编译器将更容易验证实现。但是由于我们使用的是通用条件类型,所以我们就没那么幸运了。
但无论如何,我不会太担心需要断言的实现。这些类型的意义在于 multiGroupBy()
的 调用者 获得强类型保证;它只需执行一次,您可以确保自己的操作正确,因为编译器没有能力为您执行此操作。
我有一个有两个参数的函数 1:对象数组和 2:一个字符串数组,是对象之前的键
并根据它们创建分组。如何在以下递归函数中键入结果对象值 any
?
import { groupBy, transform } from 'lodash'
const multiGroupBy = (array: objectType[], [group, ...restGroups]: string[]) => {
if (!group) {
return array
}
const currGrouping = groupBy(array, group)
if (!restGroups.length) {
return currGrouping
}
return transform(
currGrouping,
(result: { [key: string]: any }, value, key) => {
// What do I type as the any value above
result[key] = multiGroupBy(value, [...restGroups])
},
{}
)
}
const orderedGroupRows = multiGroupBy(arrayOfObjects, ['category', 'subCategory'])
所以结果将是这样的递归结构,具体取决于字符串数组的长度
const result = {
"Category Heading 1": {
"SubCategory Heading 1": [
...arrayofOfObjects
]
},
"Category Heading 2": {
"SubCategory Heading 2": [
...arrayofOfObjects
]
}
}
首先,重要的是确定 input/output 类型的 multiGroupBy()
关系。存在一系列可能的关系;在这个范围的一端,你有简单的类型,对你没有多大好处,比如无论你将什么传递到 multiGroupBy()
,输出的类型都是 object
。另一方面,你有非常复杂的类型,它可能代表输出的确切值,这取决于输入的内容......想象一下你调用 multiGroupBy([{x: "a", y: "b"}, {x: "a", y: "c"}], ["x", "y"])
的情况,并且 return 值有类型 {a: {b: [{x: "a", y: "b"}], c: [{x: "a", y: "c"}]}}
。这种类型可能很容易使用,但代价是很难实现,而且可能很脆弱。
所以我们需要在这些目的之间取得平衡。看起来有用的最简单的键入如下所示:
type MultiGroupBy<T> = (T[]) | { [k: string]: MultiGroupBy<T> };
declare const multiGroupBy: <O extends object>(
array: O[],
groups: (keyof O)[]
) => MultiGroupBy<O>;
如果您输入 O[]
类型的 array
和 Array<keyof O>
类型的 groups
,则输出为 MultiGroupBy<O>
类型。在这里,我们将输出表示为输入类型数组的 union 或数组或字典的类似字典的对象。该定义是无限递归的,并且无法指定对象的深度。为了使用它,你必须测试每个级别。
此外,当输出是一个字典时,编译器不知道这个字典的键是什么。这是一个限制,并且有解决它的方法,但这会使事情变得非常复杂,并且由于您已经接受了未知密钥,所以我不会深入讨论它。
所以让我们探索一种跟踪输出结构有多深的类型:
type MultiGroupBy<T, K extends any[] = any[]> =
number extends K['length'] ? (T[] | { [k: string]: MultiGroupBy<T, K> }) :
K extends [infer F, ...infer R] ? { [k: string]: MultiGroupBy<T, R> } :
T[];
declare const multiGroupBy: <O extends object, K extends Array<keyof O>>(
array: O[],
groups: [...K]
) => MultiGroupBy<O, K>;
现在 multiGroupBy()
接受类型 O[]
的 array
和类型 K
的 groups
,其中 K
是 constrained to be assignable to Array<keyof O>
. And the output type, MultiGroupBy<T, K>
, uses conditional types 来确定输出类型。简要地说:
如果K
是一个未知长度的数组,那么编译器会输出一些与MultiGroupBy<T>
的旧定义非常相似的东西,数组的并集或嵌套字典;如果您在编译时不知道数组长度,那确实是您能做的最好的事情。否则,编译器tries to see if the K
is a tuple认为可以拆分成它的第一个元素F
和其余元素R
的一个元组。如果可以,那么输出类型是一个字典,它的值是 MultiGroupBy<T, R>
类型……这是一个递归步骤,每递归一次,元组就会短一个元素。另一方面,如果编译器无法将 K
拆分为先行后行,则它为空...在这种情况下,输出类型为 T[]
数组。
所以打字看起来非常接近我们想要的。
不过,我们还没有完全完成。上面的类型允许 groups
中的键是 array
元素中的 any 键,包括那些 属性 值不是字符串的键:
const newArray = [{ foo: { a: 123 }, bar: 'hey' }];
const errors = multiGroupBy(newArray, ['foo']); // accepted?!
你不想允许。所以我们必须让 multiGroupBy()
的输入稍微复杂一点:
type KeysOfPropsWithStringValues<T> =
keyof T extends infer K ? K extends keyof T ?
T[K] extends string ? K : never
: never : never;
declare const multiGroupBy:
<O extends object, K extends KeysOfPropsWithStringValues<O>[]>(
array: O[], groups: [...K]) => MultiGroupBy<O, K>;
类型 KeysOfPropsWithStringValues<T>
使用条件类型查找 T
的所有键 K
,其中 T[K]
可分配给 string
。它是 keyof T
的子类型。你可以用其他方式来写,比如 KeysMatching
,但它是一样的。
然后我们将 K
约束为 Array<KeysOfPropsWithStringValues<O>>
而不是 Array<keyof O>
。现在可以使用:
const errors = multiGroupBy(newArray, ['foo']); // error!
// ----------------------------------> ~~~~~
// Type '"foo"' is not assignable to type '"bar"'.
为了确保我们对这些类型感到满意,让我们看看编译器如何看待示例用法:
interface ObjectType {
category: string;
subCategory: string;
item: string;
}
declare const arrayOfObjects: ObjectType[];
const orderedGroupRows = multiGroupBy(arrayOfObjects,
['category', 'subCategory' ]);
/* const orderedGroupRows: {
[k: string]: {
[k: string]: ObjectType[];
};
} */
看起来不错!编译器将 orderedGroupRows
视为 ObjectType
.
最后,实施。事实证明,在不使用 type assertions or any
. See microsoft/TypeScript#33912 之类的东西以获取更多信息的情况下,实现通用函数 returning 条件类型几乎是不可能的。因此,这是我在不重构您的代码的情况下所能做的最好的事情(即使我重构了它也不会变得更好):
const multiGroupBy = <
O extends object,
K extends Array<KeysOfPropsWithStringValues<O>>
>(
array: O[],
[group, ...restGroups]: [...K]
): MultiGroupBy<O, K> => {
if (!group) {
return array as MultiGroupBy<O, K>; // assert
}
const currGrouping = groupBy(array, group);
if (!restGroups.length) {
return currGrouping as MultiGroupBy<O, K>; // assert
}
return transform(
currGrouping,
(result, value, key) => {
result[key] = multiGroupBy(value, [...restGroups]);
},
{} as any // give up and use any
);
};
如果我们使用原始的联合类型,编译器将更容易验证实现。但是由于我们使用的是通用条件类型,所以我们就没那么幸运了。
但无论如何,我不会太担心需要断言的实现。这些类型的意义在于 multiGroupBy()
的 调用者 获得强类型保证;它只需执行一次,您可以确保自己的操作正确,因为编译器没有能力为您执行此操作。