Extending various TypeScript type declarations
Tags: typescript, react, node, webdev
Published: 2021-06-26 04:15:22

---
title: Extending various TypeScript type declarations
published: true
description: How to extend various TypeScript type declarations - a field guide.
tags: typescript, react, node, webdev
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/67c47o96mpssboye3w05.jpeg
---
Working with TypeScript can be a blissful experience – the type completions, the fast feedback loop, and the confidence gained by the presence of types make up for a great DX.

But yet, sometimes, these experiences are interrupted by moments of frustration. For example, maybe the library you have just pulled from npm does not expose type declarations? Or perhaps TypeScript is not aware of a global variable you **know** exists?

If that describes your experiences, read on. The following contains tips regarding extending TypeScript type declarations. I believe that by following them, the number of frustrations you experience while working with TypeScript will drastically decrease.

## Extending global type declarations

Ever written code similar to the following?

```ts
function getData({tableName: process.env.TABLE_NAME as string})
```

What about this?

```ts
/**
 * By default, TypeScript is not aware of the `Cypress` global variable available whenever the code is run in the context of a Cypress test.
 * If we do not amend the global type declarations, the error has to be silenced.
 */
// @ts-expect-error
if (window.Cypress) {
  window.myAPI = {
    /* implementation */
  };
}
```

While not a big deal, having to use [_type assertions_](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions) in these kinds of situations is not fun. Would it not be nice to have our environment variables strongly typed? Or that `Cypress` global whenever your code is run in the context of a [_Cypress_](https://docs.cypress.io/guides/overview/why-cypress) test?

By [augmenting global type declarations](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#global-augmentation), we can make sure that these, and similar, problems go away. _Type assertions_ no longer clutter our code, and the TypeScript compiler is happy. Whenever I need to extend any type declarations, I follow these steps:

1. Check what is the name of the module / interface / namespace I want to extend.
1. Create corresponding `d.ts` file. Depending on what I'm doing I might be adding changes to a file that already exist.
1. Augment the module / interface / namespace.

Let us start with the first problem - extending `process.env` type declarations to include our custom environment variables.

> Check what is the name of the module / interface / namespace I want to extend.

By hovering on `process.env` I can see that the `.env` property lives on a _namespace_ called `NodeJS`. The `.env` property is described by an interface called `ProcessEnv`.

> Create corresponding `d.ts` file. Depending on what I'm doing I might be adding changes to a file that already exist.

Since I'm augmenting global type declarations, I will create a file called `global.d.ts`. Please note that I've chosen the `d.ts` file extension on purpose. It signals to my colleges that this file only contains type declarations.

> Augment the module / interface / namespace.

Since the `.env` property lives on a namespace called `NodeJS`, I'm going to follow the [_merging namespaces_](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-namespaces) guide from the typescript handbook.

```ts
// global.d.ts
namespace NodeJS {
  interface ProcessEnv {
    TABLE_NAME: string;
  }
}
```

That is it. We can safely remove the _type assertion_ from previously shown piece of code.

```ts
function getData({tableName: process.env.TABLE_NAME})
```

Let us turn our attention to the second example - extending the `window` typings so that it includes the `Cypress` property.
The `window` global variable is annotated by `Window` interface and the `typeof globalThis`. Let us amend the `Window` interface since it's easier to do so.

```ts
// global.d.ts
interface Window {
  Cypress?: unknown; // Depending on your use-case you might want to be more precise here.
}
```

Since [_interfaces are always extendable_](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#differences-between-type-aliases-and-interfaces) that is all, we have to do. Whenever TypeScript loads the `global.d.ts` file, the `Window` interface from the built-in type declarations will be extended with our custom `Window` interface.

With that, gone is the nasty `@ts-expect-error` comment.

```ts
if (window.Cypress) {
  window.myAPI = {
    /* implementation */
  };
}
```

## Declaring type declarations for a 3rd party library

What if the new shiny library you have just pulled from the _npm_ does not come with type declarations?

In such situations, the next thing we could do is to try to pull the types for that library from the collection of community maintained types called [_DefinitelyTyped_](https://github.com/DefinitelyTyped/DefinitelyTyped). But, unfortunately, while in most cases, the type declarations that we are looking for already exist there, it's not always the case. So what should we do then?

Thankfully, the missing typings can be defined manually. To do so, I usually reach out for [_global module augmentation_](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#global-augmentation) technique that we have used earlier (the three steps process still applies to some extend).

Here is an example of adding type declarations for a library called `lib-from-npm`. The library in question exposes a `Component` function that renders a _React_ component:

```ts
// lib-from-npm.d.ts
declare module "lib-from-npm" {
    interface Props {
        // ...
    }

    function Component (props: Props) => import("React").ReactNode
}
```

An Example usage:

```tsx
// MyComponent.tsx
import { Component } from "lib-from-npm";

const MyComponent = () => {
  return <Component />;
};
```

You might be wondering what the `import("React")` statement is about. What about importing the `ReactNode` using `import {ReactNode} from 'react'`?

Let us find out what happens if I do that.

```ts
// lib-from-npm.d.ts
import { ReactNode } from 'react'

declare module "lib-from-npm" {
    interface Props {
        // ...
    }

    function Component (props: Props) => ReactNode
}
```

```tsx
// MyComponent.tsx
import { Component } from "lib-from-npm"; // TypeScript complains. Read on to learn why.

const MyComponent = () => {
  return <Component />;
};
```

I'm left with `Cannot find module 'lib-from-npm' or its corresponding type declarations` TypeScript error. It seems like the type of declarations I've just written does not work, how come?

> **Whenever the TypeScript file you are working with contains a top-level `import` statement(s), TypeScript will treat this file as a module**. **If a file is treated as a module, type declarations within that file are contained to that file only**.

This is why I've used the `import("React")` statement in the first snippet. Introduced in TypeScript 2.9, the _import types_ feature allows me to explicitly import only type declarations for a given module without using a top-level import statement. You can read more about this feature in [this excellent blog post](https://davidea.st/articles/typescript-2-9-import-types).

Having said that, this is not the only way of safely (without making TypeScript treat the definition file as a module) way of importing types to the `lib-from-npm.d.ts` file.

Here are the alternatives I'm aware of:

```ts
// lib-from-npm.d.ts

declare module "lib-from-npm" {
    import { ReactNode } from 'react'

    // Or to be even more specific
    // import type { ReactNode } from 'react';

    interface Props {
        // ...
    }

    function Component (props: Props) => ReactNode
}
```

Both alternatives work because the import statement lives in the scope of a `lib-from-npm` module. There are no top-level import(s) statements that would make this file be treated as a module by TypeScript compiler.

## Extending types of a 3rd party library

Extending types of a 3rd party library is usually no different than extending any global type declaration. The three-step process defined in the _Extending global type declarations_ section still applies.

For example, let us say that we want to add the `createRoot` API to the `ReactDOM` typings. The `createRoot` API is related to the _concurrent rendering_ the React 18 plans to introduce. Please note that the typings for the alpha release of React 18 already exist and should be preferred instead of rolling your own.

Since the `render` API of the ReactDOM package is defined within the `ReactDOM` namespace, let us extend that namespace with the `createRoot` API.

```ts
// react.d.ts
namespace ReactDOM {
  import * as React from "react";

  interface Root {
    render(children: React.ReactChild | React.ReactNodeArray): void;
    unmount(): void;
  }

  function createRoot(
    container: Element | Document | DocumentFragment | Comment
  ): Root;
}
```

As you can see I'm sticking to the principles of augmenting 3rd party library type declarations that I've defined in the previous section.
There are no top-level import(s) statements to make sure this file is **not** treated as module by the TypeScript compiler.

## Landmine

The location and the name of your `d.ts` files matters. In some unfortunate circumstances, it might happen that your `d.ts` file will be ignored.
I encountered this problem a while back, and it has stuck with me ever since. Here is the gotcha I'm talking about:

> **Whenever your `d.ts` file has the same name as a `ts` file, and both files live in the same directory, the type declarations defined in the `d.ts` file will be ignored**.

This means that going back to the previous section, If I were to create a file named `react.ts` in the same directory that the `react.d.ts` file lives, the type declarations defined in the `react.d.ts` file would be ignored.

```ts
// react.ts
import ReactDOM from "react-dom";

ReactDOM.createRoot(); // TypeScript complains.
```

As per [relevant GitHub issue discussion](https://github.com/microsoft/TypeScript/issues/31397#issuecomment-492269754) this should not be treated as a bug.

## Summary

I hope that the material presented here will help you in your day-to-day adventures with TypeScript.
The npm ecosystem is vast, and undoubtedly, one day, you will encounter a package that does not have type declarations defined for it. Whenever that moment occurs, remember about the three steps I talked about - they should help you get going with the library in no time.

You can find me on twitter - [@wm_matuszewski](https://twitter.com/wm_matuszewski)

Thank you for your time.