I have a use case where I want to exclude methods from return type of class method once they are invoked. ie, let us assume I have a class Setup having method step1, step2 and step3.
class Setup {
step1() {
return this;
}
step2() {
return this;
}
step3() {
return this;
}
}
let setup = new Setup();
My use case
- is once
step1is invoked it should return an instance of Setup which does not havestep1method at all, and users should only get the option to select betweenstep2andstep3and oncestep2is invoked it should only getstep3, asstep1andstep2were already invoked, so that a better DX can be provided - The order of execution does not matter, ie, someone can execute
step3before they executestep1. - And also, I am seeking the solution to work during runtime, ie during runtime, a step once invoked should be available for invocation itself.
let setup = new Setup();
setup
.step1()
.step2()
.step1(); // This should not be possible as step 1 was already invoked
I have already tried this, but after invoking step2 it shows step1 as an option again. I am aware that this is partially due to Omit taking Setup as the type from which it should exclude the key. But, I am unable to find a way to refer the current instance and exclude the current method.
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();
You want both the TypeScript to issue a compiler warning if someone tries to call a method more than once in their TypeScript code, and you want a runtime error if someone tries to call a method more than once at runtime. These goals are more or less independent, and you'll have to spend effort doing each one separately. It would be nice if you could just write the code that enforces your constraint at runtime and the compiler could just inspect that and behave accordingly at compile time... but the compiler's just not smart enough to do that. So in what follows let's look at each part separately.
First the type system:
The idea is to make the
Setupclass generic in thestring-constrained type parameterKcorresponding to the union of method names that should be suppressed. The default type argument isnever(K = never) because when you first create aSetupyou haven't suppressed any method names.Also, since you have
step1,step2, andstep3methods declared inSetup<K>, those methods will be present onSetup<K>no matter whatKis. That's why I definedOmitSetup<K>, which gives you a view intoSetup<K>without the methods, using theOmitutility type, and so every time you call a method with nameN, the compiler returnsOmitSetup<K | N>, addingNto the list of names to suppress.Let's walk though how it works at compile time:
So
sis aSetup<never>with nothing suppressed; when we callstep1()it returns anOmitSetup<"step1">, which does not have a knownstep1property. If you callstep2()on that, you get anOmitSetup<"step1" | "step2">, leaving you with something that only has a knownstep3method. When you call that method, you get anOmitSetup<"step1" | "step2" | "step3">, and thus all the methods are suppressed.That gives you the desired behavior:
Then at runtime:
Here each method returns a new object (this lets us re-use existing values without mutating their states, so you can write
s.step1()a million times, becausesnever changes, but you can never writes.step1().step1()). The new object copies all the properties from the current one, and also explicitly sets the property corresponding to the current method toundefined, so that nobody can call it at runtime. Let's test it out:Looks good; you can call the three methods in any order, but if you try to call the same method twice you get a runtime error.
Finally, we can marry the types to the runtime code in a single TypeScript file like this:
This is mostly just annotating the method return types as well as asserting the values returned as the intentionally loose
anytype. You actually don't needas anyhere to get it to compile, but I've included it to make it obvious to the reader that the implementation and the typings are independent. The compiler can't understand thatObject.assign(new Setup(), this, { step3: undefined })is of typeOmitSetup<K | "step3">so we are telling it not to worry.Playground link to code