TypeScript on Mars: How HubSpot Brought TypeScript to Its Product Engineers

TypeScript is a wonderful addition to the JavaScript toolbox. Static types mitigate friction as projects scale, and JavaScript projects are scaling fast. Now more than ever, users expect sophisticated and delightful browser experiences that push the limits of available technology. Since 2018, HubSpot’s infrastructure teams have used typed JavaScript to stay ahead of evolving customer expectations, tech debt and the competition at large.
There is a catch, unfortunately. While infrastructure teams like ours benefit from TypeScript, our product engineers have been left behind. Suffice it to say, TypeScript is fundamentally incompatible with the evergreen, “batteries included” tooling our product engineers use and enjoy to build frontend products.
We have high expectations for the infrastructure we provide. Our role is to empower engineers in the happiest, most productive team possible. One of our responsibilities is facilitating access to modern tools like TypeScript. So, we’ve been hard at work knocking down barriers so our engineers can enjoy the excellent TypeScript infrastructure they deserve. This has been an exciting year for our team because our work on TypeScript is finally paying off. HubSpot’s product organization is converting to TypeScript this year.
This migration is an ongoing effort with many facets; building code in TypeScript is only the first step. From an organizational perspective, we’re ramping up our investment in internal TypeScript education and support. On the technical side, we’ve also shipped new tooling for migrating to, editing, and maintaining TypeScript code. Today, we’d like to share some of the problems and solutions we’ve been working on to take TypeScript where it could never go before.
Migrating with Compassion

This year, our team’s primary focus has been ensuring that our TypeScript migration isn’t disruptive to product engineers. Switching to TypeScript is an impactful change, even under ideal circumstances, and circumstances are often not ideal. For instance, many of our engineers have little to no experience with TypeScript (or any statically typed language). Maintaining team autonomy is another point of concern. Our frontend product teams are dynamic. Some teams may be focused on performance and tooling while others are actively developing new features. We understand that TypeScript migrations won’t be an immediate priority for every team. With that in mind, our approach has been to provide flexible, self-service migration resources. With the right tools in hand, teams can create a TypeScript migration strategy compatible with their mission and available resources.
Education and support are key given our distributed migration strategy. Working with unfamiliar or poorly documented tooling all day is unsustainably exhausting. It’s imperative that engineers have free access to TypeScript learning materials and control the pace of their own TypeScript adoption. For example, we now provide engineers free access to TypeScript-focused, subscription-based online learning tools in addition to the standard learning benefits all HubSpotters enjoy. Even with access to these resources, learning takes time. Expect and instruct engineers to dedicate work hours to TypeScript learning.
Remember that engineers are often as excited about TypeScript as we are. We cannot overstate how impactful enthusiasm is during a migration. Infrastructure teams’ role typically isn’t advocating for TypeScript. Instead, focus on removing barriers to entry. Every little bit helps. Automate busy work like renaming files and adding types for props using ts-migrate
or an equivalent. Configure TypeScript and migrate a single, small file to TypeScript in each project so teams don’t need to worry about configuration steps themselves. Provide tools to identify which modules are ripe for migration or will be the most impactful when migrated. Once.
That said, it’s important to seek out and maintain a healthy dialogue with TypeScript skeptics as well. Both sides of the conversation have much to gain. Skeptics can often pinpoint weak spots in a migration strategy early, which provides valuable extra time for triage and mitigation. Switching seats, it’s vital that engineers know they’re heard and have a say in their tooling. Even if they’ve been outvoted in a general sense, skeptics deserve a compassionate migration process that accommodates their existing workflows and preferences where possible. In our experience, skeptics can even become advocates when their concerns are taken seriously.
Behind the Scenes
On the surface, HubSpotters migrating to TypeScript will see the same behavior they’d expect anywhere else. TypeScript powers editor features, runs during each build and is erased at compile time. But under the hood, our TypeScript infrastructure is unique. Now that we’ve seen HubSpot’s TypeScript migration from a human perspective, I’d like to pull the curtain back to reveal how we achieved this migration at a technical level.
First, some context. HubSpot has opinionated, integrated tooling that abstracts away common engineering needs like local development servers, linter configuration, transpilation, bundling, test automation and deployment. Our tooling is consistently a top reason that engineers love working on our product. Opinionated tooling does come with some maintenance costs, but the value we get in exchange is irreplaceable.
For the purposes of TypeScript support, the key wrinkle in our tooling is that we don’t store dependencies in node_modules
. That’s a big deal because TypeScript only knows how to find dependencies if they’re located in node_modules
. Also, unlike most other tools in the JavaScript ecosystem, TypeScript intentionally doesn’t support configuring alternate module resolution strategies. Fortunately, when there’s a will, there’s a way. We decided to fork TypeScript. While not a project to tackle lightly, we’ve found it takes a surprisingly small set of changes to support custom resolution strategies in TypeScript.
As previously mentioned, most JavaScript tools support custom resolution strategies, so there’s an abundance of prior art to work with. Almost all of these tools allow users to provide a resolve
function. Typically resolve
takes a file path and an import specifier, and then returns the path to the requested dependency. This is the heart of any module resolution strategy, and TypeScript is no different under the hood. Step one of patching TypeScript is making resolve
configurable. resolve
is also the only part of our TypeScript patch which affects type checking, which means it’s the only part of the patch which needs to remain stable over time.
1 2 3 4 5 |
// resolve('.../module.ts', 'react') -> '.../react/index.ts' function resolve(fromPath: string, identifier: string): string { // return the path of the resolved module specifier return '...'; } |
TypeScript needs a couple of other functions to support key editor features like auto imports. First, the opposite of the resolve function: getModuleIdentifier. In the same way that resolve
tells TypeScript where to find a dependency given an import statement, getModuleIdentifier tells TypeScript how to import a dependency given its location. It takes the path of two modules and returns an import specifier. Specifically, it returns the import specifier needed to import the second module from the first. Last, TypeScript needs a getAutoImportableModules function which accepts the path to a project’s root and returns a collection of module path-specifier pairs.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// getModuleIdentifier('.../module.ts', '.../react/index.ts') -> 'react' function getModuleIdentifier(fromPath: string, toPath: string): string { // return the identifier for referring to toPath from fromPath return '...'; } type ImportableModule = { path: string; identifier?: string; }; function getImportableModules(projectPath: string): ImportableModule[] { return [...]; } |
These three functions are drop-in replacements for existing logic within TypeScript. Once they’re made configurable, replace TypeScript’s default logic with the configured version to get up and running with a patched version of TypeScript. This patch is light and generally stable across TypeScript versions, so it can be rebased onto new release branches of TypeScript as they become available.
Looking into the Future
We see many opportunities to augment our existing tools now that TypeScript is in our toolbox. We can bridge the divide between frontend and backend projects with evergreen-generated API types. From there, we’re excited about building new editor features on top of TypeScript’s existing support, analyzing the type health of packages and adding type information to our code mod stack. TypeScript has a bright future at HubSpot.