Reputation: 33
I am trying to develop a fluent api in TypeScript, which I would like to use as follows:
export interface Person {
firstName: string;
lastName: string;
}
new Builder<Person>()
.func1("stringArg1")
.func2("firstName")
.func3("stringArg2");
func2
may only accept names of properties that actually occur in the specified interface (here: Person
).
Furthermore, func2
shall only be available at the return value of func1
and func3
only at the return value of func2
. The sequence of function calls must be observed in any case.
The class Builder
must be able to evaluate the specified values at any time (stringArg1
, firstName
and stringArg2
).
I would be very grateful for advices and help.
Upvotes: 0
Views: 4207
Reputation: 328097
One possible implementation of what you're asking for could be something like this:
class Builder<T> {
func1 = (f1arg: string) => ({
func2: (f2arg: keyof T) => ({
func3: (f3arg: string) => ({
// something here?
})
})
})
}
Here though we are not maintaining a single class instance of Builder
. Once you call func1()
, you get a new object with a func2
property. When you're all done, func3()
can return whatever you'd like (and it has access to the arguments due to being closed over them), including an instance of Builder
if you want. Depending on what you're looking for, this might work.
The more normal case of a Builder
class is something that, after each method, returns the same instance of that class. That's easy enough to write if you don't have restrictions on what methods can be called when. If you do have such restrictions, you can tell the compiler this, but it's a bit complicated looking. Here's one possible way to do it:
type Builder0<T> = Omit<_Builder<T>, "func2" | "func3">;
type Builder1<T> = Omit<_Builder<T>, "func1" | "func3">;
type Builder2<T> = Omit<_Builder<T>, "func1" | "func2">;
type Builder<T> = Omit<_Builder<T>, "func1" | "func2" | "func3">;
class _Builder<T> {
f1arg?: string;
f2arg?: keyof T;
f3arg?: string;
func1(f1arg: string): Builder1<T> {
this.f1arg = f1arg;
return this;
}
func2(f2arg: keyof T): Builder2<T> {
this.f2arg = f2arg;
return this;
}
func3(f3arg: string): Builder<T> {
this.f3arg = f3arg;
return this;
}
}
const Builder: new <T>() => Builder0<T> = _Builder;
In this case, each method returns this
, and at runtime it's just a "normal"-ish fluent interface. But the compiler will see new Builder<Person>()
as something that produces not a Builder<Person>
, but a Builder0<Person>
, which (if you look at the type), is the same as a builder but it uses Omit to prevent TS code from accessing its func2
or func3
method. If you call func1()
, it returns a Builder1<Person>
, which prevents TS codefrom accessing its func1
or func3
methods. So when you call func1()
followed by func2()
followed by func3()
the compiler takes a Builder0
and returns a Builder1
which returns a Builder2
which finally returns a Builder
, which omits all those methods.
You can verify that it also enforces those order restrictions while maintaining the return value as the same class instance the whole time.
Hopefully one of those approaches gives you some ideas. Good luck!
Upvotes: 2
Reputation: 2765
I don't know exactly what are you trying to achieve but here you go an example of a fluent API
export interface Person {
firstName: string;
lastName: string;
}
class Builder<T> {
private func1Value?: string;
private func2Value?: keyof T;
private func3Value?: string;
func1(arg: string): this {
this.func1Value = arg;
return this;
}
func2(key: keyof T): this {
this.func2Value = key;
return this;
}
func3(arg: string): this {
this.func3Value = arg;
return this;
}
}
new Builder<Person>().func1('stringArg1').func2('firstName').func3('stringArg2');
Upvotes: 5