Typescript一旦被调用就会从类中排除方法



我有一个用例,我想在调用类方法后从返回类型中排除方法。例如,让我们假设我有一个类Setup具有方法step1,step2step3

class Setup {
step1() {
return this;
}

step2() { 
return this;
}

step3() { 
return this;
}
}
let setup = new Setup();

我的用例

  1. 是一旦step1被调用,它应该返回一个没有step1方法的Setup实例,用户应该只得到在step2step3之间选择的选项,一旦step2被调用,它应该只得到step3,因为step1step2已经被调用,这样可以提供更好的DX
  2. 执行顺序无关紧要,即有人可以在执行step1之前执行step3
  3. 而且,我正在寻求在运行时工作的解决方案,即在运行时,一旦调用的步骤应该可用于调用本身。
let setup = new Setup();
setup
.step1()
.step2()
.step1(); // This should not be possible as step 1 was already invoked

我已经尝试过了,但是在调用step2之后,它再次显示step1作为一个选项。我知道这部分是由于省略将Setup作为应该排除键的类型。但是,我无法找到一种方法来引用当前实例并排除当前方法。

export type Omit<A extends object, K extends string> = Pick<A, Exclude<keyof A, K>>
class Setup {
step1(): Omit<Setup, 'step1'> {
return this;
}

step2(): Omit<Setup, 'step2'>{ 
return this;
}

step3():Omit<Setup, 'step3'>{ 
return this;
}
}
let setup = new Setup();

你希望当有人在TypeScript代码中多次调用一个方法时,TypeScript都会发出一个编译器警告;你希望当有人在运行时多次调用一个方法时,会出现一个运行时错误。这些目标或多或少是独立的,你必须分别花时间去完成每一个目标。这将是很好的,如果你可以只是写的代码,在运行时强制你的约束和编译器可以检查,并相应地在编译时的行为…但编译器还不够聪明。因此,在接下来的内容中,让我们分别查看每个部分。


首先键入system:

type OmitSetup<K extends string> = Omit<Setup<K>, K>;
declare class Setup<K extends string = never> {
step1(): OmitSetup<K | "step1">;
step2(): OmitSetup<K | "step2">;
step3(): OmitSetup<K | "step3">;
}

这个想法是使Setup类在string约束类型参数K中泛型,对应于应该被抑制的方法名称的联合。默认类型参数是never(K = never),因为当您第一次创建Setup时,您没有抑制任何方法名称。

此外,由于您在Setup<K>中声明了step1,step2step3方法,因此无论K是什么,这些方法都将出现在Setup<K>中。这就是为什么我定义了OmitSetup<K>,它使用Omit实用程序类型为您提供了一个没有方法的Setup<K>视图,因此每次调用名称为N的方法时,编译器都会返回OmitSetup<K | N>,将N添加到要抑制的名称列表中。

让我们看看它在编译时是如何工作的:

const s = new Setup();
// const s: Setup<never>
const s1 = s.step1();
// const s1: OmitSetup<"step1">
const s12 = s1.step2();
// const s12: OmitSetup<"step1" | "step2">
const s123 = s12.step3();
// const s123: OmitSetup<"step1" | "step2" | "step3">

所以s是没有被抑制的Setup<never>;当我们调用step1()时,它返回一个没有已知step1属性的OmitSetup<"step1">。如果你在上面调用step2(),你会得到一个OmitSetup<"step1" | "step2">,留给你一个只有已知step3方法的东西。当你调用这个方法时,你会得到一个OmitSetup<"step1" | "step2" | "step3">,因此所有的方法都被抑制了。

给你想要的行为:

s.step1().step2().step3(); // okay
s.step2().step1().step3(); // okay
s.step1().step2().step1(); // error!
// -------------> ~~~~~
// Property 'step1' does not exist on type 'OmitSetup<"step1" | "step2">'. 
// Did you mean 'step3'?

然后在运行时:

class Setup {
step1() {
console.log("step1");
return Object.assign(new Setup(), this, { step1: undefined });
}
step2() {
console.log("step2");
return Object.assign(new Setup(), this, { step2: undefined });
}
step3() {
console.log("step3");
return Object.assign(new Setup(), this, { step3: undefined });
}
}

这里每个方法返回一个新对象(这让我们可以重用现有的值而不改变它们的状态,所以你可以写s.step1()一百万次,因为s永远不会改变,但你永远不能写s.step1().step1())。新对象复制当前对象的所有属性,并且还显式地将当前方法对应的属性设置为undefined,因此没有人可以在运行时调用它。让我们测试一下:

const s = new Setup();
s.step1().step2().step3(); // "step1", "step2", "step3"
s.step2().step1().step3(); // "step2", "step1", "step3"
s.step1().step2().step1(); // "step1", "step2", RUNTIME ERROR!
// s.step1().step2().step1 is not a function

看起来不错;你可以按任意顺序调用这三个方法,但如果你试图两次调用同一个方法,你会得到一个运行时错误。


最后,我们可以将这些类型与运行时代码结合在一个TypeScript文件中,像这样:

type OmitSetup<K extends string> = Omit<Setup<K>, K>;
class Setup<K extends string = never> {
step1(): OmitSetup<K | "step1"> {
console.log("step1");
return Object.assign(new Setup(), this, { step1: undefined }) as any
}
step2(): OmitSetup<K | "step2"> {
console.log("step2");
return Object.assign(new Setup(), this, { step2: undefined }) as any
}
step3(): OmitSetup<K | "step3"> {
console.log("step3");
return Object.assign(new Setup(), this, { step3: undefined }) as any
}
};

这主要是注释方法返回类型,以及将返回的值断言为故意松散的any类型。实际上,这里不需要as any来编译它,但是我包含它是为了让读者明白实现和类型是独立的。编译器不能理解Object.assign(new Setup(), this, { step3: undefined })OmitSetup<K | "step3">类型,所以我们告诉它不要担心。


Playground链接到代码

编辑

既然你已经说了目标是没有函数本身,你可以把它重新分配给undefined,这样实际上就没有函数了。

除此之外,你还可以通过正确排除多个类型来进行Typescript推理。

class Setup {
step1(): Omit<Setup, 'step1'> {
(this as any).step1 = undefined;
return this;
}

step2(): Omit<Setup, 'step1' | 'step2'> { 
return this;
}

step3(): Omit<Setup, 'step1' | 'step2' | 'step3'> { 
return this;
}
}

这样,step1实际上被删除了,不能被调用,并且会抛出TypeError step1 is undefined, Typescript将正确地单独显示step2,step3

最新更新