Back

TechnologyMay 30, 2019

TypeScript: Adding Custom Type Definitions for Existing Libraries

Jose Gonzalez

One of the biggest pain points I’ve faced with TypeScript is using it in strict mode (without disabling several warnings and errors) while using external library dependencies in my project. Whether or not a library has type definitions is a big factor in deciding whether I’ll use it. You can find the type definitions for many libraries, but sometimes they don’t exist and you have no choice of other libraries since there’s only one that does what you need. In these circumstances, you have to add your own custom type definitions for the libraries. This article will show you how to that.

code notes

The code referenced in this blog post can be found on Github. You can find usage of all the libraries and/or components for which we create definitions inside of the DemoComponent.tsx file. The project uses React, but it’s not necessary to know React to understand the article.

The example project was created using the default React creator and the command npx create-react-app typescript-example --typescript and then modified to use older type definition files so as to have incomplete definitions and allow us to create the missing custom definitions in the project.

If you’re following along with the example project, please note some of the examples may have had definitions added to them since the time of writing. For the third-party libraries, we’re simply not adding in the definitions to the example project. For the Array example, I forced the project to use the ES2015 TypeScript SDK definitions, which was missing a function’s definition. For the React DOM element (ordered list) example I’ve forced the project onto an older version of React types definitions so that it’s missing the property we’re extending. If you want to run the project don’t forget to run npm install on it, before npm run.

initial setup

Before you write custom types, you need to tell the transpiler about them. To do this you should edit the tsconfig.json file, and add the typeRoots property under the compilerOptions property.

{ "compilerOptions": { //...other properties "typeRoots": [ "src/customTypings", "node_modules/@types" ] }}

When the property is excluded, TypeScript is automatically searching for types in the node_modules/@types folder of your project. In this case, we’re going to make a new folder inside our src folder called customTypings to hold all our definition files, and then point TypeScript to it.

Inside of that folder you’ll be adding definition files, which by convention have a file extension of .d.ts. While I like to name them with the name of the library or component I’m extending, the name does not actually matter, only the contents do.

adding a basic definition for a library using defaultexport

After the initial setup, we need to import a library. In this example we’re going to import the classnames library, which has a very simple usage:

import classnames from 'classnames';

This is performing a defaultExport import from the library (more information on how imports work here), and our goal is to stop TypeScript from sending warnings and errors and allow us to use the library. You can find this in the empty-types.d.ts file in the project:

declare module 'classnames' { const noTypesYet: any; export default noTypesYet;}

The module’s name must match the library’s import name exactly—“classnames”, in this case. We then create a default export of type “any”, which allows us to use the library in any way we want. On the upside, this method requires the least amount of effort, but unfortunately it also provides the least amount of help when it comes to using TypeScript, since it doesn’t provide auto-complete or type checking.

adding a complex definition for a library using a specific class

For this example we’ll be extending types for react-d3-components. We’ll be trying to create a BarChart with it, by importing the React component, using it in conjunction with JSX and adding some properties to it. (Note: The following snippet shows only what’s directly relevant to this example, but it won’t work if you copy/paste this into a file. See DemoComponent.tsx for the fully runnable code.)

import { BarChart } from 'react-d3-components';   var data = [{ label: 'somethingA', values: [{ x: 'SomethingA', y: 10 }, { x: 'SomethingB', y: 4 }]}];   <BarChart data={data} height={400} width={600} margin={{ top: 10, bottom: 50, left: 50, right: 10 }}/>

The above describes a class that extends a React component which has four properties:

  1. Data – an array of objects, each with a “label” property (string) and “values” property (array of objects, each with string properties “x” and “y”)

  2. Height – number

  3. Width – number

  4. Margin – an object with four properties, all numbers, named “top”, “bottom”, “left” and “right”

By looking at the documentation of the library and knowing what properties I plan to use on my project, I came up with the following definition, which can be found in the react-d3-components.d.ts file:

declare module 'react-d3-components' { export class BarChart extends React.Component<BarChartProps & any, any> {   }   interface BarChartProps { data: BarChartData[] width?: number margin?: MarginValues }   export interface BarChartData { label: string values: BarChartValue | BarChartValue[] }   export interface BarChartValue { x: string, y: number }   export interface MarginValues { top?: number bottom?: number left?: number right?: number }}

Again, we start off with declaring a module having the exact same name as the library. In React, all components are classes which extend React.Component. With the addition of TypeScript, we can also declare what properties and state the component has by using the generics format React.Component<Props, State>. Properties are what’s passed into the component and state is related to variables saved inside the component. When writing a definition, you only care about declaring the properties, since that’s the only part you interact with outside the library. You can think of the properties you’re defining as a contract or interface to interact with the component.

Notice how in the example above our property type is BarChartProps & any. We do this so we get auto-complete and type checking for the explicit properties we’re going to give it, while allowing us to still use any other property which we have not explicitly declared. This way we can look at the library’s documentation and implement changes without having to update the type definition. For example, we’re using height in the component, even though it’s not declared in the type definition.

The rest of the properties we discussed earlier are declared in the definition, allowing us to get auto-complete and validation when implementing the component. The export of those interfaces allows us to import it just like we would do for the BarChart, itself.

extending an incomplete typescript sdk definition

There are times when the TypeScript SDK does not include definitions for a property or function already supported by some browsers. In this example it’s the function array.includes() which is missing. If you face this scenario where the missing definition is part of the basic TypeScript definitions, and not a part of a library, all you need to do is re-declare the interface and add the missing property. To know what to add, I referenced the Mozilla Developers Network’s JS documentation and built it from that. You can find this in array.d.ts:

interface Array<T> { includes: (item: T, fromIndex?: number) => boolean }

If you find yourself unsure of exactly what you need to write, you can use this method: Type out a different function for which there is a definition (e.g., fill), and then let Visual Studio take you to the definition using the built-in code navigation of the IDE. This opened up the lib.es2015.core.d.ts file (located at C:\Program Files (x86)\Microsoft SDKs\TypeScript\3.0 on my computer). Inside you’ll find the following excerpt, which can give you an idea of what to write:

interface Array<T> { fill(value: T, start?: number, end?: number): this;}

extending an incomplete third-party library definition

An even more common scenario is finding third-party libraries with existing definitions that are incomplete. Often the people writing the definitions are not the same as the people writing the libraries, so they can become outdated. In this example we’ll be extending React’s definitions, and use the following JSX of an ordered list:

<ol type="a" color="blue" start={3}> <li> first item in list </li></ol>

Since this example uses React, this DOM element ol uses React’s type definitions, but the type attribute does not exist in them. The solution is writing the below definition in orderedListHtmlElement.d.ts:

import 'react';   declare module 'react' { interface OlHTMLAttributes<T> { type?: "1" | "a" | "A" | "i" | "I"; }}

Notice how we’re importing the React library despite not using it. If we don’t import it first, our module declaration overrides the module declared in React’s index.d.ts definitions file, which breaks everything else. We also cannot extend the interface by omitting the module declaration and just importing the interface and extending it like this:

import { OlHTMLAttributes } from 'react'; interface OlHTMLAttributes<T> { ... })

The above won’t work because it’s not part of the TypeScript SDK, but rather it’s from an existing library (React) with existing definitions, and so it must be treated slightly differently.

The step-by-step process to get to that definition is started by adding a property that does exist, and going to the definitions of that, which opens up React’s index.d.ts file, found in /node_modules/@types/react/. In the DOM example above, the color property works, but it’s a property that’s shared among several different DOM elements.

interface HTMLAttributes<T> extends DOMAttributes<T> { color?: string; }

You can then search the file for HTMLAttributes and find the following definition:

interface OlHTMLAttributes<T> extends HTMLAttributes<T> { start?: number;}

If instead of choosing color we had chosen start as the property to dig into, we would’ve gotten there from the beginning.

Seeing the above interface, and knowing we only want to add type to the ordered list element, we can write the definition declared above.

learn more

I hope you found this information useful, as it should provide you with some important—and somewhat obscure—details to help you add custom type definitions to existing TypeScript libraries. Everything relevant to this article can be found in the customTypings folder and DemoComponent.tsx. If you have any additional questions feel free to reach out to us at findoutmore@credera.com.