【问题标题】:How to get the type of a recursive object as function argument in typescript?如何在打字稿中获取递归对象的类型作为函数参数?
【发布时间】:2021-10-09 19:35:06
【问题描述】:

我有一个带有 2 个参数的函数 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
    ]
  }
}

Here is a codesandbox 获取代码。

【问题讨论】:

  • 请考虑修改这个问题中的代码以构成一个minimal reproducible example,当它放入像The TypeScript Playground (link)这样的独立IDE中时,清楚地展示了您面临的问题,没有不相关的错误,额外代码,或缺少声明。这将使那些想要帮助您的人立即着手解决问题,而无需首先重新创建它。它将使您得到的任何答案都可以针对定义明确的用例进行测试。
  • 你还没有定义objectType,我不确定你的输出中是否需要强类型键(如果编译器知道,例如, "Category Heading 1"result 的一个键?或者如果它只知道它有一些 string 键而不知道它们是什么,这可以吗?)
  • 根据您在打字中需要多少信息,您可以使用this。如果这对你有用,我可以写一个答案;如果没有,请详细说明缺少的内容。
  • 非常感谢您输入它看起来有点复杂。我已经通过上面的代码框链接添加了更多信息,并输入了 lodash 库,您可以查看更多详细信息。你能检查一下是否可以使用更简单的打字方式吗?
  • "看起来有点复杂" 是的,如果你需要一个强类型的输出,那么输入就有点复杂了。您能解释一下您希望看到来自multiGroupBy(arrayOfObjects, ["category", "subCategory"]) 的返回类型吗?你不想要any,或者像object 这样的微不足道的东西。下一个最简单的是type Ret = Record<string, Ret | ObjectType[]>。之后你可以拥有Record<string, Record<string, ObjectType[]>,然后你开始让编译器跟踪特定的键,比如{"Category Heading 1": {"SubCategory Heading 1": ObjectType[]}, ...}

标签: javascript typescript lodash


【解决方案1】:

首先确定multiGroupBy() 所需的输入/输出类型关系很重要。有一系列可能的关系;在这个范围的一端,你有一些对你没有多大好处的简单类型,比如无论你传递什么到multiGroupBy(),输出的类型都是object。另一方面,您有非常复杂的类型,可能代表输出的确切值,具体取决于输入的内容...想象一下您调用 multiGroupBy([{x: "a", y: "b"}, {x: "a", y: "c"}], ["x", "y"]) 的情况,返回值具有 type{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[] 类型的arraygroups 类型Array&lt;keyof O&gt;,则输出类型为MultiGroupBy&lt;O&gt;。在这里,我们将输出表示为输入类型数组或数组或字典的类字典对象的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() 接受array 类型O[]groups 类型K,其中Kconstrained 可分配给Array&lt;keyof O&gt;。而输出类型MultiGroupBy&lt;T, K&gt; 使用conditional types 来确定输出类型。简要说明:

如果K 是一个未知长度的数组,那么编译器将输出与MultiGroupBy&lt;T&gt; 的旧定义非常相似的东西,即数组或嵌套字典的联合;如果您在编译时不知道数组长度,那确实是您能做的最好的事情。否则,编译器tries to see 如果K 是一个tuple,可以拆分为它的第一个元素F 和其余元素的一个元组R。如果可以,则输出类型是字典,其值的类型为MultiGroupBy&lt;T, R&gt;... 这是一个递归步骤,每次进行递归时,元组都会缩短一个元素。另一方面,如果编译器无法将 K 拆分为 first-and-rest,那么它就是空的……在这种情况下,输出类型是 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&lt;T&gt; 类型使用条件类型来查找 T 的所有键 K,其中 T[K] 可分配给 string。它是keyof T 的子类型。你可以用其他方式写,比如 KeysMatchingthis answer,但都是一样的。

然后我们将K 限制为Array&lt;KeysOfPropsWithStringValues&lt;O&gt;&gt; 而不是Array&lt;keyof O&gt;。现在可以使用:

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 assertionsany 之类的东西,实现返回条件类型的泛型函数或多或少是不可能的。有关更多信息,请参阅microsoft/TypeScript#33912。所以这是我在不重构代码的情况下能做的最好的事情(即使我重构了它也不会变得更好):

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()调用者 获得强类型保证;它只需要实现一次,你可以注意你做对了,因为编译器没有为你做这件事。


Stackblitz link to code

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-03-30
    • 2021-12-15
    • 2022-11-11
    • 1970-01-01
    • 1970-01-01
    • 2020-10-25
    • 2022-01-25
    • 1970-01-01
    相关资源
    最近更新 更多