Reputation: 21480
I have some builder class that implements interface that it is expected to build.
But I want to make one method of this class required to call. By required I mean compiletime, not a runtime check.
Class is supposed to be used as a chain of method calls and then be passed to a function as an Interface it implements. Preferably to require method right after the constructor, but that's not really needed.
Example: playground
interface ISmth {
x: number;
y?: string[];
z?: string[];
}
class SmthBuilder implements ISmth {
x: number;
y?: string[];
z?: string[];
constructor(x: number) {
this.x = x;
}
useY(y: string) {
(this.y = this.y || []).push(y)
return this
}
useZ(z: string) {
(this.z = this.z || []).push(z)
return this
}
}
declare function f(smth: ISmth): void
f(new SmthBuilder(123)
.useY("abc") // make this call required
.useZ("xyz")
.useZ("qwe")
)
Upvotes: 0
Views: 545
Reputation: 21480
Typescript allows not to specify interface explicitly. So if you don't, you can change it a little so that it won't be compatible with interface anymore... Until you'll patch it a little in useY
call: playground
interface ISmth {
x: number;
y?: string[];
z?: string[];
}
class SmthBuilder {
x: ISmth["x"];
y?: ISmth["y"] | "You have to call 'useY' at least once";
z?: ISmth["z"];
constructor(x: number) {
this.x = x;
}
useY(y: string): { y: ISmth["y"] } & this {
(this.y = this.y as ISmth["y"] || []).push(y)
return this as any
}
useZ(z: string) {
(this.z = this.z || []).push(z)
return this
}
}
declare function f(smth: ISmth): void
f(new SmthBuilder(123)
.useY("abc") // this call is required
.useZ("xyz")
.useZ("qwe")
)
If you want to force useY
to be called exactly once, you have to change only useY
definition to
useY(y: string): { y: ISmth["y"] } & this & Omit<this, "useY"> {
Remember that these things are working only if you chain the calls. If you save instance to a variable, the type of variable is frozen, but the internal state changes from call to call.
Upvotes: 0
Reputation: 329783
My inclination would be to extend ISmth
to denote that useY()
has been called, like this:
interface ISmthAfterUseY extends ISmth {
y: [string, ...string[]];
}
Then your SmthBuilder
's useY()
method can return an ISmthAfterUseY
:
useY(y: string) {
(this.y = this.y || []).push(y)
return this as (this & ISmthAfterUseY);
}
And your f()
function, if it cares about getting an ISmth
with a defined, non-empty y
property, should ask for an ISmthAfterUseY
and not an ISmth
:
declare function f(smth: ISmthAfterUseY): void
f(new SmthBuilder(123)
.useY("abc")
.useZ("xyz")
.useZ("qwe")
) // okay
f(new SmthBuilder(123).useZ("xyz")) // error!
// Types of property 'y' are incompatible.
Okay, hope that helps; good luck!
Upvotes: 2