engineering grouparoo node.js react typescript
2020-10-16
↞ See all posts
Two of the major components of the @grouparoo/core
application are a Node.js API server and a React frontend. We use Actionhero as the API server, and Next.JS for our React site generator. As we develop the Grouparoo application, we are constantly adding new API endpoints and changing existing ones.
One of the great features of Typescript is that it can help not only to share type definitions within a codebase, but also across multiple codebases or services. We share the Typescript types
of our API responses with our React Frontend to be sure that we always know what kind of data we are getting back. This helps us ensure that there is a tight coupling between the frontend and backend, and that we will get compile-time warnings if there’s something wrong.
In Actionhero, all API responses are defined by Actions, which are classes. The run()
method of the Action class is what is finally returned to the API consumer. Here’s a prototypical example of an action that lets us know what time it is:
1import { Action } from "actionhero"; 2 3export class GetTime extends Action { 4 constructor() { 5 super(); 6 this.name = "getTime"; 7 this.description = "I let you know what time it is"; 8 this.inputs = {}; 9 this.outputExample = {}; 10 } 11 12 async run() { 13 const now = new Date(); 14 return { time: now.getTime() }; 15 } 16}
This action takes no input, and returns the current time as a number
(the unix epoch in ms). The action is also listed in our config/routes.ts
file as responding to GET /time
.
The next step is to extract the run()
method’s return type to get the type
of the API response
We can use a helper like type-fest
’s PromiseValue
to get the return value, or we can do it ourselves:
1// from https://www.jpwilliams.dev/how-to-unpack-the-return-type-of-a-promise-in-typescript 2 3export type UnwrapPromise<T> = 4 T extends Promise<infer U> 5 ? U 6 : T extends (...args: any) => Promise<infer U> 7 ? U 8 : T extends (...args: any) => infer U 9 ? U 10 : T;
So, the type of the Action’s response is:
1type ActionResponse = UnwrapPromise<typeof GetTime.prototype.run>; // = { time: number; }
And in our IDE:
This is excellent because now any changes to our action will result in the type
being automatically updated!
The Grouparoo Application is stored in a monorepo, which means that the frontend and backend code always exist side-by-side. This means that we can reference the API code from our Frontend code, and make a helper to check our response types. We don't need our API code at run-time, but we can import the types
from it as we develop and compile the app to Javascript.
The first thing to do is make a utility file which imports our Actions and extracts their types. Grouparoo does this in web/utils/apiData.ts
1import { UnwrapPromise } from "./UnwrapPromise"; 2import { GetTime } from "../../api/src/actions/getTime"; 3 4export namespace Actions { 5 export type GetTime = UnwrapPromise<typeof GetTime.prototype.run>; 6}
This apiData.ts
will allow us to more concisely reference Actions.GetTime
in the rest of our react application.
Now, to use the Action’s response type, all we have to do is assign it to the response of an API request:
1import { useState, useEffect } from "react"; 2import { Actions } from "../utils/apiData"; 3 4export default function TimeComponent() { 5 const [time, setTime] = useState(0); 6 7 useEffect(() => { 8 load(); 9 }, []); 10 11 async function load() { 12 const response: Actions.GetTime = await fetch("/api/time"); 13 setTime(response.time); 14 } 15 16 if (time === 0) return <div>loading...</div>; 17 18 const formattedTime = new Date(time).toLocaleString(); 19 return <div>The time is: {formattedTime}</div>; 20}
Now we have enforced that the type of response
in the load()
method above will match the Action, being { time: number; }
. We will now get help from Typescript if we don’t use that response value properly as a number. Foe example, assigning it to a string variable creates an error.
Since Typescript is used at “compile time”, it can be used across application boundaries in surprisingly useful ways. It’s a great way to help your team keep your frontend and backend in sync. You won’t incur any runtime overhead using Typescript like this, and it provides extra certainty in your test suite that your frontend will use the data it gets from your API correctly.
I write about Technology, Software, and Startups. I use my Product Management, Software Engineering, and Leadership skills to build teams that create world-class digital products.
Get in touch