To tackle this problem, we can start by introducing a new method called protected
. This method will act as a delegate for all calls to functions like startFoo()
, startBar()
, and so on. Let's name this method startify()
and have it accept an additional argument named type
:
class C {
#fsm = {
send(type: string, name: string) {
console.log("I was sent type \"" + type + "\" and name \"" + name + "\"");
}
}
protected startify(type: string, name: string) {
this.#fsm.send(type, name);
return this;
}
}
Next, we need to include the startXxx
methods in the class prototype and inform the compiler about these methods available in class instances. Here's how we can achieve that:
const methodNames = ["foo", "bar", "baz"] as const;
The methodNames
array consists of all lowercase names after start
. By applying a const
assertion, we maintain the literal types of the strings within the array.
type MethodNames = typeof methodNames[number];
The MethodNames
represents the union of string literal types in methodNames
, which in this case is "foo" | "bar" | "baz"
.
type CMethods = { [K in MethodNames as `start${Capitalize<K>}`]: { (name: string): C } };
In CMethods
, keys are remapped from MethodNames
with the initial letter capitalized using Capitalize<T>
utility function. These mapped keys signify the method names ("startFoo"
, "startBar"
, "startBaz"
) while values represent methods that take a string
argument and return a value of type C
.
interface C extends CMethods { }
By incorporating declaration merging, each instance of C
now encompasses all methods in CMethods
along with those declared inside class C {}
.
In conclusion, the compiler recognizes the added methods, but they need to be implemented. We create a utility function called capitalize()
followed by dynamically adding methods to C.prototype
for each element in
methodNames</code to delegate to <code>startify
using
the call()
method.
Testing our implementation:
const c = new C();
c.startBar("barf").startFoo("food")
// Output:
// I was sent type "BAR" and name "barf"
// I was sent type "FOO" and name "food"
Success! The compiler expects c.startBar()
to exist and return a C
for chaining calls. At runtime, the delegation to startify
works seamlessly.
View code on TypeScript Playground