Photo by Elke Karin Lugert on Unsplash

Our front-end team at Hopjump has been working with TypeScript for a while now. When we’re working in TypeScript, everything is wonderful – we have quick editor feedback to see if we’re writing type errors, automatic IntelliSense to see possible properties and methods, and wonderful editor tools to help us write TypeScript quicker.

TypeScript works great for the application code we write – however most projects nowadays use at least some third-party libraries. How can we use libraries, while still keeping the great developer experience that TypeScript gives us?

Option 1: The third-party library is written in TypeScript

This is the best option for sure, we get types for free! When a project is written in TypeScript, the bundled code that is shipped to NPM is compiled to JavaScript since that’s what runs natively in node or the browser. In addition to this JS bundle, the TypeScript types that are used internally in the library are also shipped. These types go to an index.d.ts file and end up in the node_modules directory for that project.

For instance, react-content-loader is written in TypeScript. Here’s a tree of the file structure:

A glimpse into the node_modules/react-content-loader directory

The yellow outline shows the code as it will run in our apps, and the blue outline shows the types that the TypeScript compiler will use.

Great! This also ensures that the types are always in sync with the code, since the library wouldn’t compile if there were type errors. In practice, these types work great:

Viewing the type definitions for lodash's pick function

We can easily see all of the functions available in the library, as well as the types of the parameter and return values. You can also command click (control click on Windows) on the import in VSCode to see the type definitions for the entire module. This can be a very useful reference!

However, not all third-party libraries are written in TypeScript. As soon as we try to use a library that doesn’t have TypeScript types, many of the benefits of TypeScript get thrown away:

Come on, IntelliSense!
RIP

We’re back in the world of untyped JavaScript.

We could decide to use libraries solely based on if the library was written in TypeScript, but that’s generally not a good idea. We could be missing out on some great libraries, like React! So, what do we do if the library wasn’t written in TypeScript?

Option 2: Import @types/library-name

TypeScript provides another way to get types for third-party libraries, even if they aren’t written in TypeScript through a project called DefinitelyTyped.

DefinitelyTyped is a massive GitHub repository that stores types for most JavaScript libraries. It’s structured as a monorepo, so types for every compatible JavaScript library are present here. These types are published to the npm namespace @types, e.g. @types/react or @types/styled-components.

GitHub is a little broken on the DefinitelyTyped repo since it's so large

These types are community maintained, which means that users of the library edit these types. It’s not exactly the wild west, though, because there are “type maintainers” that will review any changes to the types and merge the changes. Since these are community maintained, it’s very likely that these types are up to date, even if the official library doesn’t support types on their own.

In addition to being community maintained, DefinitelyTyped is officially “blessed” by the TypeScript team and Microsoft. This gives a few benefits:

  1. If type maintainers are particularly unresponsive, a stale PR to update types will be escalated to the TypeScript team and somebody there will review and merge the PR.
  2. The TypeScript team uses these library types as a “test suite” for the TypeScript language itself – this means that any breaking changes in the language are tested against existing types, and they will update the types if there are errors with newer versions of the language.
  3. The tooling around publishing types is very documented and streamlined, because it’s being maintained by a large company (Microsoft).
  4. DefinitelyTyped isn’t going to randomly disappear, because it’s core to the TypeScript language. It’s not some random user’s type definition monorepo.

Great! This lets us have high quality types for packages, even if they’re written in JavaScript. But, what happens if the types for a library we’re using aren’t present in DefinitelyTyped? The preferred option would be to create the types and publish them to DefinitelyTyped (and give back to the community), but that’s not always feasible. Sometimes there are custom types you’d like to add which don’t work with the library in general. For those cases, you may want to define your own types.

Option 3: Define your own types

Creating our own types is pretty easy in TypeScript. By default, TypeScript will automatically pick up any type definitions that are inside the @types directory in node_modules, as well as any index.d.ts files. This means you can create an index.d.ts file wherever you want in your project, and TypeScript will use it. At Hopjump, we organize the types inside the types directory in our application:

Example file structure for custom types

Inside the index.d.ts file, you can declare all the types you want for that package. Make sure to declare the module or namespace that you want the types to be defined inside. Read about this more in the TypeScript documentation. Here’s an example where we added types to window-or-global:

declare module "window-or-global" {
  declare let root: Window
  export = root
}
Example types for window-or-global

Because this code is wrapped in the window-or-global namespace, whenever we import something from window-or-global it will use these types.

Bonus: Monkeypatching types

Sometimes types in DefinitelyTyped are wrong. In those cases, we might want to fix those types by applying type monkeypatches. In general this is a bad idea – it’s much better to submit a PR to the DefinitelyTyped repo to fix the types for everyone, but sometimes this is useful.

The start is the same --  create an index.d.ts file for the library you’re trying to patch. Inside that file you will declare the module as you did before, and stick the new types inside that module. However, since you want to “merge” these new types with the existing types, you should also import the original types inside that type file. Here’s an example for @testing-library/react-hooks:

// inside types/testing-library__react-hooks/index.d.ts
import "@testing-library/react-hooks"

declare module "@testing-library/react-hooks" {
  interface RenderHookResult {
    // This function is not included in the typings as of 3.2.1
    wait(
      callback: (...args: any[]) => any,
      options?: { timeout?: number; suppressErrors?: boolean },
    ): Promise<void>
  }
}
Example monkeypatch for @testing-library/react-hooks

Now, when you import @testing-library/react-hooks it will include the original types, plus the new function defined.

Conclusion

Hopefully this clears up why third-party types are important, as well as clarify the various ways these types can make it into our application. Availability of robust type definitions may also be a criteria for evaluating a new third-party library, since good accurate types can help you develop faster. If a library doesn’t have types, TypeScript also gives you the flexibility to write them yourself and contribute them back to the community.

Happy typing!