如何在打字稿中为方法链声明递归类型?

How to declare recursive types in typescript for method chaining?

我想在打字稿中制作方法链 class。 (比如 a.add(3).add(3).mul(3),)

但在我的例子中,根据语法指南,只有部分方法可以在方法之后访问

例如,在A方法之后,A和B方法可用。 B法之后,B法和C法可用。

我执行它喜欢跟随和成功。类型检测和 linter 工作得很好,但我认为这不是正确的实施方式。 running code is in here

class ABC {
  private log: string[]
  public constructor() {
    this.log = [];

    this.A = this.A.bind(this);
    this.B = this.B.bind(this);
    this.C = this.C.bind(this);
    this.getLog = this.getLog.bind(this);
  }

  public A() {
    this.log.push('A');
    return { A: this.A, B: this.B, getLog: this.getLog };
  }

  public B() {
    this.log.push('B');
    return { B: this.B, C: this.C, getLog: this.getLog };
  }

  public C() {
    this.log.push('C');
    return { C: this.C, getLog: this.getLog };
  }

  public getLog() {
    return this.log;
  }
}

const abc = new ABC();
const log = abc.A().A().A().B().B().C().getLog();
console.log('log: ', log);

因为我必须关心每个方法的 return 类型,因为 class

中可能有近百种方法

所以我想要的是在一个对象中管理所有方法语法(方法可用性?方法后可访问的方法列表?),并根据该语法生成​​ class 接口。

正如你在下面看到的,我试图大致制作我想要的东西。 但也许是因为递归,这种类型不能正常工作:( 有办法解决这个问题吗? 或者有什么好的方法可以满足我的要求?

我认为最好是'Chain'类型

类型 a = 类型链

//和我上面实现的class ABC的类型一样

// information of function state machine
const Syntax = {
  A: {
    next: ['A', 'B'] as const,
  },
  B: {
    next: ['B', 'C'] as const,
  },
  C: {
    next: ['C'] as const,
  },
};

interface Chain {
  A(): Pick<Chain, typeof Syntax.A.next[number]>;
  B(): Pick<Chain, typeof Syntax.B.next[number]>;
  C(): Pick<Chain, typeof Syntax.C.next[number]>;
  getLog(): string;
}

class ChainABC implements Chain{
  ~~
}

为class ABC running code is in here

重新附上播放代码url

这太复杂了,我什至认为我无法正确解释它。首先,我会将您的基础 class 更改为 return 只是 this 的内容,用于您打算使其可链接的方法。在运行时这本质上是一样的;只有 compile-time 类型定义是错误的:

class ABC {
  private log: string[] = [];

  public A() {
    this.log.push("A");
    return this;
  }

  public B() {
    this.log.push("B");
    return this;
  }

  public C() {
    this.log.push("C");
    return this;
  }

  public getLog() {
    return this.log;
  }
}

然后我想描述如何将 ABC 构造函数解释为 ChainABC 构造函数,因为两者在运行时是相同的。

让我们想出一种方法来确定 class 的可链接方法...我会说它们只是那些 function-valued 属性 return 的值与 class 实例相同的类型:

type ChainableMethods<C extends object> = {
  [K in keyof C]: C[K] extends (...args: any) => C ? K : never
}[keyof C];

并且当您将 ABC 转换为 ChainABC 时,您将需要一种类型将此类可链接方法映射到满足此约束的其他可链接方法的联合:

type ChainMap<C extends object> = Record<
  ChainableMethods<C>,
  ChainableMethods<C>
>;

最后我们将描述ChainedClass<C, M, P>,其中C是要修改的class类型,M是方法链接映射,P是我们希望在结果中存在的特定键:

type ChainedClass<
  C extends object,
  M extends ChainMap<C>,
  P extends keyof C
> = {
  [K in P]: C[K] extends (...args: infer A) => C
    ? (
        ...args: A
      ) => ChainedClass<
        C,
        M,
        | Exclude<keyof C, ChainableMethods<C>>
        | (K extends keyof M ? M[K] : never)
      >
    : C[K]
};

这是递归的……而且很复杂。基本上 ChainedClass<C, M, P> 看起来像 Pick<C, P>C 中的可链接方法被 return ChainedClass<C, M, Q> 的方法替换,其中 Q 是正确的键集来自 C.

然后我们创建将 ABC 构造函数转换为 ChainABC 构造函数的函数:

const constrainClass = <A extends any[], C extends object>(
  ctor: new (...a: A) => C
) => <M extends ChainMap<C>>() =>
  ctor as new (...a: A) => ChainedClass<C, M, keyof C>;

咖喱化是因为我们要推断AC但需要手动指定M.

下面是我们如何使用它:

const ChainABC = constrainClass(ABC)<{ A: "A" | "B"; B: "B" | "C"; C: "C" }>();

看看 M 类型是如何 {A: "A" | "B"; B: "B" | "C"; C: "C"},代表你想放置的约束,我想。

正在测试:

const o = new ChainABC()
  .A()
  .A()
  .A()
  .B()
  .B()
  .C()
  .C()
  .getLog();

行得通。如果你检查 Intellisense,你会注意到你不能在 A() 之后调用 C() 并且你不能在 B() 之后调用 A() 并且你不能调用 A()B()C().

之后

好的,希望对你有帮助;祝你好运!

Link to code