Types are great. We’re pretty sold on them here at Nous. They allow us to move fast with confidence, and during our current phase of quickly iterating towards Product Market Fit, they’re helping us optimise for speed of feature development and Developer Experience. And it seems we’re not alone, as typed languages and retrofitted type systems are fast becoming the norm.
Annotating your code with types brings the machine into the conversation. And that dialogue is hugely valuable – it lets the computer verify what you’re doing (i.e. type checking), suggests what you should be doing (rich IDE support i.e. code completion), and prevents a whole class of errors from ever reaching your users. Once you’re used to this compiler back-and-forth, developing in lone silence feels very unsettling (we also love an analogy here at Nous).
Type-checking in the context of a single “deployable” is straightforward. Type-checking across boundaries (database to server, server to client, client to cache, etc.) much less so. To get all the benefits mentioned above, we need external data to be strongly typed. And that inevitably* means runtime validation.
Runtime validation is in itself straightforward (check out yup, io-ts, and zod if you haven’t already). The problem is maintenance. Updating a database schema means updating types and validators in your backend. Updating an endpoint means updating types and validators in your frontend. And all this code is generally boilerplate heavy and error prone…
Enter type generation. We want these types and validators to be derived from the code they describe. Here at Nous we’re using two awesome technologies for this: Prisma and GraphQL. Prisma is a “Next-generation Node.js and TypeScript ORM”. It uses a single, database-agnostic schema file to describe your models, which is then used to generate database migrations and a TypeScript client. GraphQL also uses a schema file, but to describe an API. The GraphQL schema can then generate client code as well as server code, all with validation builtin. 👌
As a closing point, it’s worth noting that all this typing and code generation is not a silver bullet. There are still plenty of edge cases you need to look out for. A backend service could deploy despite a failed database migration, meaning the prisma client is now out of sync. A frontend client could be running some cached code, meaning the GraphQL client is out of sync. In these cases your last line of defence is proper versioning.
Overall we’ve now been using Typescript for 6 months on the Front-End and full stack for the last two. Since implementing end-to-end, it’s delivering on its promise of allowing us to move fast and so far hasn’t broken too many things.
*If you have a lot of confidence in your types/code generation, you might want/need to skip runtime validation as performance optimization. But in my experience this is rarely a good idea.
No-one should be dealing with the cost-of-living crisis all alone. We’re building a new service to liberate households from drudgery and make people’s lives simpler and fairer.