How to create your own TypeScript type definition files (.d.ts)?


TypeScript, a superset of JavaScript, offers static typing capabilities that enhance code quality and catch errors during compilation. To fully leverage TypeScript's static typing features, it's crucial to have type definition files (.d.ts) for external JavaScript libraries and modules used in projects. These type definition files describe the types and interfaces exposed by the external entities, enabling the TypeScript compiler to understand their shape and behaviour.

In this article, we'll explore the step-by-step process of creating custom TypeScript type definition files, empowering developers to benefit from static typing in their projects.

Prerequisites

Type definition files (.d.ts) serve as an interface between JavaScript code and the TypeScript compiler. They describe the types, functions, classes, and modules that exist within a JavaScript library or framework. These files enable the TypeScript compiler to perform type checking and provide rich IntelliSense support within an IDE.

Before we dive into creating type definition files, make sure you have the following prerequisites set up −

  • A basic understanding of TypeScript.

  • TypeScript is installed globally on your system. You can install it using npm with the following command −

npm install -g typescript

Step 1: Determine the Library Structure

To create a type definition file, you need to understand the structure of the library or module you are creating typings for. This involves analysing the JavaScript code, understanding the exposed functions, classes, objects, and their corresponding types.

Step 2: Start With an Empty .d.ts File

Create an empty .d.ts file with the same name as the JavaScript library or module you are creating typings for. For example, if your library is called "my-library," the type definition file should be named "my-library.d.ts".

Step 3: Declare the Module

The first step in creating a type definition file is to declare the module using the declare module syntax. This tells TypeScript that you are defining types for a specific module or library. Here's an example −

declare module 'my-library' {
   // Type definitions go here
}

Step 4: Define Types and Interfaces

Within the module declaration, you can define the types and interfaces that describe the structure of the library. You can declare interfaces for objects, types for functions, and more.

Let's consider an example where we have a library called "math-library" with a single function add that takes two numbers as arguments and returns their sum −

declare module 'math-library' {
   export function add(a: number, b: number): number;
}

In the example above, we declare a module named "math-library" and export a function add that takes two numbers (a and b) as parameters and returns a number.

Step 5: Export Types and Interfaces

To make the defined types and interfaces available to other TypeScript files, you need to export them explicitly. Use the export keyword before each type or interface declaration. Here's an example extending our previous math-library example −

declare module 'math-library' {
   export function add(a: number, b: number): number;

   export interface Calculator {
      name: string;
      add(a: number, b: number): number;
   }
}

In the updated example, we export a new interface called Calculator that has a name property and a method add with the same signature as the add function we defined earlier.

Step 6: Use Ambient Declarations

Ambient declarations allow you to describe types for existing JavaScript code that does not have TypeScript-specific annotations. They are enclosed in a declare global block. Here's an example −

declare global {
   interface Array<T> {
      filter(callbackfn: (value: T, index: number, array: T[]) => boolean): T[];
   }
}

In the example above, we declare a global interface Array<T> to extend the built-in Array type. We add a new method filter and specify its signature using a callback function that takes a value, index, and the array itself as parameters.

Step 7: Reference External Type Definitions (Optional)

If your type definition file depends on other external type definition files, you can reference them using the /// <reference types="..."/> syntax. For example, if your library relies on the type definitions of the popular library "lodash," you can include the reference as follows −

/// <reference types="lodash" />

This ensures that the TypeScript compiler includes the type definitions from "lodash" while compiling your code.

Step 8: Test and Refine

Once you have defined the type definition file, it's essential to test it with your codebase. Import or require the library in your TypeScript project and use the defined types and interfaces. If the TypeScript compiler throws any errors or if the behaviour of the library doesn't match the defined types, go back to your type definition file and refine it accordingly.

Example 1

Let's consider a practical example.

Let’s create a module named my-library which has a utility function capitalize which takes a string and returns the capitalized version of it.

my-library.js

function capitalize(st) {
   return st.charAt(0).toUpperCase() + st.slice(1);
}
module.exports = {
   capitalize: capitalize,
};

Now, we will create a type definition file to restrict the usage of the exported capitalize function with compile-time type checking −

First, create a new file named my-library.d.ts with following content −

declare module 'my-library' {
   export function capitalize(str: string): string;
}

In the example above, we declare a module named "my-library" and export a function capitalize that takes a string parameter and returns a string.

You can now use the capitalize function in your TypeScript code with type safety. We create an app.ts file now to use our capitalize function −

app.ts

/// <reference types="node" />
const { capitalize } = require("./my-library");
const result = capitalize("hello");
console.log(result); // Output: Hello

By adding the /// <reference types="node" /> directive, you inform TypeScript to use the installed Node.js type definitions for the require function. Now, TypeScript should recognize the require function.

By providing accurate type definitions, the TypeScript compiler can validate the usage of the capitalize function, preventing any potential type errors.

Output

It will produce the following output −

Hello

Conclusion

Creating TypeScript type definition files (.d.ts) allows developers to harness the power of static typing and enhance the developer experience when working with external JavaScript libraries and modules. By following the step-by-step process of analysing the library structure, declaring modules, defining types and interfaces, and testing and refining the type definitions, developers can ensure accurate and comprehensive typings for their projects. With type definition files in place, TypeScript provides improved code quality, early error detection, and enhanced maintainability. By investing the time to create precise type definitions, developers can fully leverage the benefits of TypeScript in their development workflow.

Updated on: 01-Aug-2023

2K+ Views

Kickstart Your Career

Get certified by completing the course

Get Started
Advertisements