Ship More Frequently 🚀

by Automatically Generating TypeScript Types

Leila Adams
FloSports Engineering

--

Here at FloSports we ❤️ types.

Why?

Stronger, more accurate types prevent bugs and create a premium, authentic user experience. 🙅🏻 🐛

Another reason?

Better types make developers happy since there’s less guesswork, less defensive coding, and less overall wasted energy preventing bugs. Software engineers are able to more effortlessly create new features with confidence. It’s engineering rocket fuel 🚀

Solid types are like engineering rocket fuel
Photo by SpaceX on Unsplash

Wait… What are types again?

Types are all the possible values of a given variable. Here is an example of a Card type that has four properties. Two properties, label and description, might have string or null values. The other two properties, title and action, will always be defined, at least presumably.

In programming, it’s important to have accurate types to prevent giving incorrect instructions to software. In the example above, if we attempt to access card.action.url, but the Card type is incorrect and action can sometimes be null or undefined, a runtime error will result:

Uncaught TypeError: Cannot read properties of null (reading 'url')

Types are hard y’all!

It is challenging to create a perfect type system, as type definitions must correctly persist across data stores, services and clients. One of our challenges: our NestJS micro services are written in TypeScript, yet there is no easy way to share types with our web app, which is also written in TypeScript. That’s right — repos we created and control have a difficult time sharing a contract in the same language. That doesn’t seem right. 🤔

So in the meantime…

What do we do to make up for this lacking? We copy over types from backend to frontend, we hardcode them, we duplicate and debate them. They become outdated (or were never accurate to begin with) and downstream a bug emerges that annoys our users (and hopefully they don’t unsubscribe because of it).

Simply put: it’s not a system, it’s chaos.

The Solution

ts-morph to the rescue!

ts-morph to the rescue!
Photo by Neil Mark Thomas on Unsplash

The ts-morph library wraps the TypeScript compiler API so you can very simply and quickly extract interfaces from your NestJS classes and publish them as a types package to share across projects.

An example of what that code looks like:

You can do the same for type aliases and enums. You can even omit problematic properties, exclude members, recursively include base class interfaces, and convert enums to type unions.

ts-morph is a powerful library!

Basic Developer Steps

  1. npm install ts-morph in your NestJS project as a dev dependency
  2. Create a .ts file for your first morph
  3. Run ts-node ./my-first-morph.ts (create a script in package.json)
  4. Import Project from ts-morph and get files from your NestJS project
  5. Extract interfaces, type aliases, and enums into a new file
  6. Typecheck new file by running tsc
  7. Publish the new types file as a package

The Outcome

Rocket Fuel 🚀

We have a major initiative at Flo — to build the Next Gen experience for our subscribers and be the ESSENTIAL DESTINATION for all things related to your favorite sport! With this ambitious initiative comes GOALS. We needed rocket fuel to get there and thankfully, this sped up development across our web app teams, making it easier and quicker to ship three major features this past quarter.

Our Next Gen experience is more lit because we shared types across two projects. Imagine if we shared types across our ENTIRE engineering ecosystem? 🔥

Developer Experience 😄

Developers also seem happier because they no longer have to hunt through API code to find the class models and then manually type out interfaces. Now you npm install the latest types package and you go on your merry way!

Pro Tips

Warning ⚠️

As the name implies, ts-morph can easily morph project code. It’s a library commonly used for large-scale, programmatic refactors. For this use case, our goal is to only mutate the new types file. So, be careful kids, and keep an eye on your changed files before committing.

Human-free 🤖

Automatically generate the types file and publish the types package any time a new tag is created as part of a hands-free (and human-free) build pipeline. The goal is an improved developer experience, so set up an automatic publish of the types where no one is required to do anything, only the bots.

Set Boundaries 🙅🏻‍♂️

Use TypeScript configuration files to control the scope and boundaries of your NestJS project vs. your types package as they live side-by-side in the same repository. For example, in your main project’s tsconfig.json, you should probably exclude the types package to avoid conflicts.

"include": ["src"],"exclude": ["node_modules", "dist", "tests", "types-pkg"],

🐪 vs 🐍

No doubt the published types will be camel cased, but the outputted JSON from the API could be snake case depending on the serialization. So the types might not match the JSON (datePublished vs date_published). We simply created an interceptor in our web app that used a camel-case package to convert the JSON response.

Decoding 🚮

On the client, you might still want to decode API responses with the types from this new ts-morph generated types package. Throw out JSON that doesn’t comply since it’s still possible to get bad data from your persistence layer. This will give you 100% certainty. We use io-ts for this, a runtime type system for IO decoding/encoding. And you don’t have to throw out the ENTIRE response. For example, you can simply omit certain items in an array if they do not match the object schema.

Mobile Types 📱

We could take this to the next level and also generate Swift and Kotlin types from our new TypeScript file using the quicktype library. Its TypeScript support, however, is experimental at the moment and, among other things, doesn’t support translating all type aliases. For example, it errors on Record<string, string>, which you would have to replace with { [key: string]: string } 🙈

However, once those kinks are ironed out, generating mobile types could be as easy as installing quicktype and running a command on your new TypeScript file.

quicktype types-pkg/index.ts -o models.swift

Onwards!

We’ve come a long way with TypeScript, but there are still so many opportunities for improvement when it comes to server and client sharing type information. The solution I proposed here is for projects in separate repos. For a monorepo solution, check out tRPC, end-to-end typesafe APIs made easy. In the future, it would be fantastic to have a code-less solution no matter how your source files are stored. Maybe with the right combination of configuration and innovation, we will get there soon. For now, ts-morph is a great way to get developers out of a frustrating cycle of type guessing every feature and on the front foot with product delivery.

--

--