Qwertiy
Qwertiy

Reputation: 21480

Enforce the method to be called

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

Answers (2)

Qwertiy
Qwertiy

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

jcalz
jcalz

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!

Playground link

Upvotes: 2

Related Questions