网站搜索

输入 TypeScript 泛型的重复模式


我想为一个通用函数接受无限参数,该函数将有效键与.连接在一起。理论上,这可以接受的参数数量与嵌套对象的数量一样多。

以下代码片段可以正常工作,但我想知道它是否可以接受任意数量的参数,而不仅仅是我手动输入的那些?

const tt = <Lang>() => {
  return <
    A extends keyof Lang,
    B extends keyof Omit<Lang[A], keyof string>,
    C extends keyof Omit<Lang[A][B], keyof string>,
    D extends keyof Omit<Lang[A][B][C], keyof string>,
    E extends keyof Omit<Lang[A][B][C][D], keyof string>,
    F extends keyof Omit<Lang[A][B][C][D][E], keyof string>
    // Can this repeat without manually typing each out?
  >(
    a: A,
    b?: B,
    c?: C,
    d?: D,
    e?: E,
    f?: F,
  ) => {
    return [a, b, c, d, e, f].filter(Boolean).join('.');
  };
};

console.log(tt<{Parent: {child: 'child'}}>()('Parent', 'child'));
// Console: "Parent.child"

代码可以按原样工作,但它应该能够接受无限数量的参数。

也许像这样:

const tt = <Lang>() => {
  return <A extends SomethingGoesHere>(...args: A) => {
    return args.join('.');
  };
};

console.log(tt<{Parent: {child: 'child'}}>()('Parent', 'child'));
// Console: "Parent.child"

一个后续问题:我讨厌为了让泛型类型工作,这必须是一个高阶函数。为什么 TypeScript 不允许我根据提供的泛型类型 Lang 推断 A-F?

在理想的世界里,我本可以做到这一点:

const tt = <
  Lang,
  A extends keyof Lang,
  B extends keyof Omit<Lang[A], keyof string>,
  C extends keyof Omit<Lang[A][B], keyof string>,
  D extends keyof Omit<Lang[A][B][C], keyof string>,
  E extends keyof Omit<Lang[A][B][C][D], keyof string>,
  F extends keyof Omit<Lang[A][B][C][D][E], keyof string>
>(
  a: A,
  b?: B,
  c?: C,
  d?: D,
  e?: E,
  f?: F,
) => {
  return [a, b, c, d, e, f].filter(Boolean).join('.');
};

tt<{Parent: {child: 'child'}}>('Parent', 'child');
// ^ error: Expected 7 type arguments, but got 1.ts(2558)

但是它抛出了那个错误。

答案#1

@TitianCernicova-Dragomir 是对的,这里实际上没有更好的办法(至少没有受支持的方法)。有一件事通常比与 TypeScript 的类型系统斗争更有效,那就是将代码重构成它更容易理解的形式。

例如,如果不是支持可变参数函数,而是创建一个可链式调用的函数,每次调用都会将当前类型 T 缩小为它的某个属性 T[K],会怎么样?类似这样的:

type ObjStrKey<T> = T extends object ? Extract<keyof T, string> : never;

interface Keychain<T> {
  <K extends ObjStrKey<T>>(k: K): Keychain<T[K]>;
  value: string;
}

function keychain<T>(): Keychain<T> {
  function kc<T, K extends ObjStrKey<T>>(
    this: Keychain<T>,
    k: K
  ): Keychain<T[K]> {
    const value = (this.value ? this.value + "." : "") + k;
    const v = { value };
    return Object.assign(kc.bind(v), v);
  }
  const v = { value: "" };
  return Object.assign(kc.bind(v), v);
}

其工作原理如下:

interface Person {
  parent: Person;
  child: Person;
  spouse: Person;
  name: string;
}

const kcp = keychain<Person>();
console.log(kcp("child")("spouse")("child")("parent")("name").value);
// child.spouse.child.parent.name

代码链接

这对编译器来说很容易,因为每个函数调用只执行一次属性查找。

如果你觉得弄 this 和 bind 有点奇怪,你可以将其改为常规的 class 并将函数调用改为方法调用,像这样:

type ObjStrKey<T> = T extends object ? Extract<keyof T, string> : never;

class Keychain<T> {
  static make<T>() {
    return new Keychain<T>("");
  }
  private constructor(public value: string) {}
  k<K extends ObjStrKey<T>>(k: K): Keychain<T[K]> {
    const value = (this.value ? this.value + "." : "") + k;
    return new Keychain<T[K]>(value);
  }
}

这有点不同:

const kcp = Keychain.make<Person>();
console.log(kcp.k("child").k("spouse").k("child").k("parent").k("name").value);
// child.spouse.child.parent.name

代码链接

无论如何,这只是一个想法。希望有帮助,祝你好运!