如何在Typescript中使用递归类型?

vatpfxk5  于 2023-04-07  发布在  TypeScript
关注(0)|答案(1)|浏览(154)

我试图理解如何在Typescript中进行递归类型输入,并获得编译错误。
我有以下两个功能-

  • getNestedProperty-递归函数返回对象的嵌套属性的值
  • sum-接受对象数组并对嵌套属性的值求和的函数
function getNestedProperty<T extends Record<string, T>, P extends keyof T>(obj: T, keys: string[]): T | T[P] {
  const nestedKeys = [...keys];
  const nestedKey = nestedKeys.shift();
  if (nestedKey !== undefined) {
    return getNestedProperty(obj[nestedKey], nestedKeys) as T;
  } else {
    return obj;
  }
}

export function sum<T extends number | Record<string, T>>(array: T[], keys?: string[]): number {
  const totalSum = array.reduce((accumulator: number, element: T) => {
    if (typeof element === 'number') {
      return accumulator + element;
    }
    if (keys === undefined) {
      throw new Error(`keys not passed in as an argument and type of Array is not number`);
    }
    return accumulator + +getNestedProperty(element, keys);
  }, 0);
  return totalSum;
}

我在尝试调用sum函数时得到此错误

const array = [
  { a: { b: 1 } }, 
  { a: { b: 3 } }, 
  { a: { b: 5 } }
];

const actual = Utils.sum(array, ['a', 'b']);
expect(actual).to.equal(1+3+5);

Error:
Argument of type '{ a: { b: number; }; }[]' is not assignable to parameter of type '(number | Record<string, { a: { b: number; }; }>)[]'.
  Type '{ a: { b: number; }; }' is not assignable to type 'number | Record<string, { a: { b: number; }; }>'.
    Type '{ a: { b: number; }; }' is not assignable to type 'Record<string, { a: { b: number; }; }>'.
      Property 'a' is incompatible with index signature.
        Property 'a' is missing in type '{ b: number; }' but required in type '{ a: { b: number; }; }'.ts(2345)
e4eetjau

e4eetjau1#

generic函数的调用签名类似于

declare function foo<T extends number | Record<string, T>>(t: T): void;

当你用一些对象字面量调用它时

foo({ a: 1 }) // error!

编译器没有使用number | Record<string, T>作为T的 * 推断点 *(如建议的那样,但从未在microsoft/TypeScript#7234中采用)。相反,它仅从传入的值推断T,然后根据约束 * 检查 * 它。
因此在上面的调用中,T被推断为{a: 1},因为这是{a: 1}文字的类型。然后根据number | Record<string, T>进行检查,number | Record<string, T>number | Record<string, {a: 1}>。并且{a: 1}不 * 扩展number | Record<string, {a: 1}>,因此值不满足约束,并且存在错误。
调用成功的唯一方法是如果传入的值已经知道是以所需的方式递归的,但如果传入的是对象文字,则不会发生这种情况。
所以我们需要一个不同的方法。
除非您需要泛型,否则您可以将函数设置为非泛型,并在必要时使它们作用于适当的递归类型,如

type Summable = number | { [k: string]: Summable };

因此,之前的值将针对该类型进行检查,结果成功:

const val: Summable = { a: 1 }; // okay

所以你可以重构为:

function sum(array: Summable[], keys?: string[]): number {
  const totalSum = array.reduce((accumulator: number, element) => {
    if (typeof element === 'number') {
      return accumulator + element;
    }
    if (keys === undefined) {
      throw new Error(`keys not passed in as an argument and type of Array is not number`);
    }
    return accumulator + Number(getNestedProperty(element, keys));
  }, 0);
  return totalSum;
}

也许

function getNestedProperty(obj: unknown, keys: string[]): unknown {
  const nestedKeys = [...keys];
  const nestedKey = nestedKeys.shift();
  if ((nestedKey !== undefined) && (typeof obj === "object") && obj && (nestedKey in obj)) {
    const o = obj as Record<string, unknown>;
    return getNestedProperty(o[nestedKey], nestedKeys);
  } else {
    return obj;
  }
}

这里我们只说getNestedProperty(obj, keys)的输入和输出类型是unknown,因为如果不使函数通用化,就没有什么更有用的了。
请注意,我可能会将实现重构为

function getNestedProperty(obj: unknown, keys: string[]): unknown {
  return keys.reduce((acc, k) =>
    typeof acc === "object" && acc && k in acc ?
      (acc as Record<string, unknown>)[k] : acc,
    obj);
}

除非你需要递归。
如果您确实需要使函数具有通用性,那么您必须经历一个相对痛苦的过程,即准确描述函数将执行的操作。

type DeepIdx<T, K extends PropertyKey[]> =
  K extends [infer K0, ...infer KR extends PropertyKey[]] ?
  K0 extends keyof T ? DeepIdx<T[K0], KR> : T : T;

function getNestedProperty<T, K extends PropertyKey[]>(
  obj: T,
  keys: [...K]
): DeepIdx<T, K> {
  return keys.reduce<any>((acc, k) =>
    typeof acc === "object" && acc && k in acc ? acc[k] : acc,
    obj);
}

尽管我不想进一步离题来解释它是如何工作的。
Playground链接到代码

相关问题