Better Distinctions of Builder Immutablility
See original GitHub issueWhich package is the feature request for?
discord.js
Feature
Builders does it’s job adequately for what it’s meant for, however there are some issues when repurposing it for discord.js. Functionally it operates the same, however the way it’s returned can be confusing to say the least.
Builders imply mutability regardless of where they’re used
A good example of this is embeds from messages. The setX
methods, data
are all fully public and exposed. Even when the data shouldn’t be mutable (because it’s from the API).
Another example is unintentionally inaccurate types. Let’s take SelectMenuInteraction
for example, when you receive a select menu interaction we can get the component like so:
const { component } = selectMenuInteraction;
Ok great, nothing out of the ordinary, now let’s check the type of the customId
:
const id: string = component.customId // Error: string | undefined is not assignable to string
Wait what? How is customId null-able. After all, you can’t send in a select menu without a custom id? This is because the types here are representative of a class with mutable/changing state. In the context of a builder string | undefined
is a perfectly acceptable type since the structure hasn’t been fully built.
Yes, toJSON indicates a finalized state, but only if you’re using raw API data. In discord.js this isn’t the case, so there should be new structures to hold immutable api data.
Ideal solution or implementation
Introduce immutable (Built) classes
Many of the most widely used patterns for builders include a build()
. This method simply signals that the builder is complete, and its state will always remain immutable via a new class.
For example:
class Car {
public readonly wheels: Wheels;
public readonly windows?: Windows;
public readonly seats: Seats;
constructor(data: ...) {
this.wheels = data.wheels;
this.windows = data.windows;
this.seats = data.seats;
}
public drive() {
console.log('vroom!');
}
}
class CarBuilder {
public wheels?: Wheels;
public windows?: Windows;
public seats?: Seats;
setWheels(wheels: Wheels) { ... }
setWindows(windows: Windows) { ... }
setSeats(seats: Seats) { ... }
build() {
// Validate required inputs (we can use toJSON for this)
return new Car({ ...this });
}
}
const car: Car = new CarBuilder()
.setWheels(new Wheels())
.setSeats(new Seats())
.build();
car.drive(); // Prints "vroom!"
Ideally this could be applied within discord.js so there’s a better and safer distinction to what is already built
and what is currently a builder
.
Note: the pattern above is a common OOP builder pattern. See these resources for references:
- https://www.baeldung.com/kotlin/builder-pattern
- https://javadevcentral.com/effective-java-builder-pattern
- https://medium.com/@martinstm/fluent-builder-pattern-c-4ac39fafcb0b
Alternative solutions or implementations
No response
Other context
No response
Issue Analytics
- State:
- Created 2 years ago
- Reactions:4
- Comments:5 (5 by maintainers)
Top GitHub Comments
So I’ve been thinking this through and there’s one more thing that should change to allow this.
Remove getters from builders and relocate them to built structures
This sounds controversial at first but hear me out.
Builders are a means to building a finalized structure, they aren’t the structures themselves. When structures are used/consumed usually it’s via getters/props. Builders having getters for props doesn’t make sense if a finalized version of it is added. In addition, moving getters from a builder to the built structure presents less confusion about the purpose of a built structure vs a builder (one is for making a structure, one is the built structure that can be passed around. It prevents people from trying to repurpose mutable builders as structures and gives incentive to finalize them via a
build()
method.Note: Builders would still expose their
data
field for access to properties, however this would ideally only be used for situations where the library can’t handle something that you need.This would solve 3 issues:
Message
in the future, djs can easily extend the finalized version of a message builder rather than try to override types and methods to make it work in djs. (Trying to extend a mutable message builder in djs would not be fun)Along with this structures would be renamed to align with the changes for example
SelectMenuComponent
->SelectMenuBuilder
Then,
SelectMenuBuilder#build
returns a finalizedSelectMenuComponent
If anyone has any ideas on how to implement this without duplicating all the getters from builders I’m all ears. Having a hard time coming up with an implementation 😅