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
step1
is invoked it should return an instance of Setup which does not havestep1
method at all, and users should only get the option to select betweenstep2
andstep3
and oncestep2
is invoked it should only getstep3
, asstep1
andstep2
were already invoked, so that a better DX can be provided - The order of execution does not matter, ie, someone can execute
step3
before 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();
2
Answers
Edit
Now that you’ve said the aim is to not have the function itself, you can just reassign it to
undefined
so there actually is no function.On top of it, you can also have Typescript inference by correctly excluding multiple types.
This way, the
step1
is actually deleted and cannot be called and will throwTypeError step1 is undefined
and Typescript will correctly showstep2
,step3
alone.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
Setup
class generic in thestring
–constrained type parameterK
corresponding to the union of method names that should be suppressed. The default type argument isnever
(K = never
) because when you first create aSetup
you haven’t suppressed any method names.Also, since you have
step1
,step2
, andstep3
methods declared inSetup<K>
, those methods will be present onSetup<K>
no matter whatK
is. That’s why I definedOmitSetup<K>
, which gives you a view intoSetup<K>
without the methods, using theOmit
utility type, and so every time you call a method with nameN
, the compiler returnsOmitSetup<K | N>
, addingN
to the list of names to suppress.Let’s walk though how it works at compile time:
So
s
is aSetup<never>
with nothing suppressed; when we callstep1()
it returns anOmitSetup<"step1">
, which does not have a knownstep1
property. If you callstep2()
on that, you get anOmitSetup<"step1" | "step2">
, leaving you with something that only has a knownstep3
method. 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, becauses
never 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
any
type. You actually don’t needas any
here 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