如何在打字稿中声明递归类型以进行方法链接?

时间:2019-07-16 12:57:36

标签: typescript class method-chaining

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

但是在我的情况下,根据语法指南,在方法之后只能访问部分方法

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

我实现它就像跟随和成功一样。 type detect和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);

因为我必须关心每个方法的返回类型,因为类中可能有数百种方法

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

正如您在下面看到的,我试图大致做出我想要的。 但也许由于递归,这种类型不能正常工作:( 有什么办法可以解决这个问题? 还是有很酷的方式满足我的要求?

我认为最好使用类似“ Chain”的类型

type a = type Chain

//与我上面实现的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{
  ~~
}

重新附加ABC running code is in here类的播放代码网址

1 个答案:

答案 0 :(得分:2)

这很复杂,我什至认为我无法正确解释。首先,我将您的基类更改为只返回this的对象,以使您打算使其可链接的方法。在运行时,这基本上是相同的。只是错误的编译时类型定义:

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构造函数,因为两者在运行时是相同的。

让我们提出一种确定类的可链接方法的方法……我要说的是,这些函数值属性返回的值与类实例的类型相同:

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是要修改的类类型,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中的可链接方法由返回ChainedClass<C, M, Q>的方法代替,其中Q是{{1 }}。

然后我们制作将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>; A,但需要手动指定C

这是我们的使用方式:

M

我认为,const ChainABC = constrainClass(ABC)<{ A: "A" | "B"; B: "B" | "C"; C: "C" }>(); 类型是M,代表您想放置的约束。

测试:

{A: "A" | "B"; B: "B" | "C"; C: "C"}

那行得通。您会注意到,如果您检查Intellisense,则在const o = new ChainABC() .A() .A() .A() .B() .B() .C() .C() .getLog(); 之后不能调用C(),在A()之后不能调用A()并且不能呼叫{{ 1}}或B()之后的A()

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

Link to code