At Grouparoo, we use a lot of TypeScript. We are always striving to enhance our usage of strong TypeScript types to make better software, and to make it easier to develop Grouparoo. Strong types make it easy for team members to get quick validation about new code, and see hints and tips in their IDEs - a double win!
Recently, I found myself repeating a lot of metadata when defining a new API endpoint as I was working to enable
noImplicitAny within the
@grouparoo/core project. We use Actionhero to build Grouparoo, and so a typical Action might look like:
Notice how the params provided back to the
run() method are typed, even though we provide that information functionally via the
formatter argument to the Action's inputs. Defining this information in both locations was tedious, and more nefariously, a possible place for drift between the implementation and the types. What would it take for TypeScript to automatically be able to determine the types of our Params?
I tried many approaches to programmatically determine the types of an Action's params, and learned a lot along the way. The most interesting thing that I learned was that method argument types are not inherited in TypeScript. Initially, I wanted to modify the abstract base
Action class to automatically reflect its input types into the run method, but it's not possible:
Consider the following:
Greeter, the fact that the
greet() method is re-implemented means that the initial type of the method from the abstract class can't be assumed. After hitting that dead end, I pivoted to attempt to build a transformation utility type. While working on this, I found myself inspecting the properties of the
Action class in question, and I learned was that TypeScript doesn't really know what goes on in a Class constructor.
For example, you can define the same class both ways:
At runtime, these 2 classes will have the same behavior, with
this.items = ["apple", "banana"], but because the class property was defined strictly in
StaticList, we can get the literal types back, rather than just the "string" we get from
Knowing the above, it became clear that to reach the goal, I would need to reformat all of our action definitions to not use a constructor. After that, TypeScript can start to inspect the properties of the class. Our utility can take in the Action's class as an argument, and inspect both the keys of the
inputs, and if there is a
formatter present, infer its return type:
Of note, because we are accepting data over HTTP or websocket most commonly, we can assume that an input without a formatter is a string.
Putting everything together, here's what our final Action looks like:
And finally, we can see our params are typed:
We contributed this work back to Actionhero, and in Actionhero v28.1.0, the
ParamsFrom utility is included!