Mastering Declaration Files: The Key to TypeScript’s Type Magic

Unlock the Potential of Declaration Files to Enhance Your JavaScript Development

Irene Smolchenko
ITNEXT

--

Welcome! In this article, we’ll explore TypeScript’s declaration files and learn how to effectively utilize them in your code. 🤩
By the end, you’ll have a solid understanding of writing declaration files, emitting them during compilation, augmenting modules, and discovering the benefits of DefinitelyTyped and @types. Let’s dive right in!

A chessboard with a pawn and a king, symbolizing the journey and evolution towards mastery in a particular skill or domain.
Photo by Praveen Thirumurugan on Unsplash

What are Declaration Files?

Declaration files in TypeScript are used to provide type information about JavaScript libraries or modules that don’t have built-in TypeScript support. They allow you to use external JavaScript code in a TypeScript project while still benefiting from TypeScript’s static type checking.

These files usually have the .d.ts extension and include type declarations for variables, functions, classes, interfaces, and other elements found in a JavaScript code.

Suppose you have a JavaScript library called math-library.js that contains the following code:

// math-library.js
function calculateSum(a, b) {
return a + b;
}

const PI = 3.14159;

module.exports = {
calculateSum,
PI,
};

To use it in TypeScript and have type checking, you can create a declaration file called math-library.d.ts with the following contents (we will demonstrate methods to create such files in the upcoming sections):

// math-library.d.ts
declare function calculateSum(a: number, b: number): number;
declare const PI: number;

export { calculateSum, PI };

By using the declare keyword we define the type information for the variables and functions exported from the JavaScript code.

By importing the functions and variables from the declaration file, you can use them in your TypeScript code with type checking and IntelliSense support.

// app.ts
import { calculateSum, PI } from "./math-library";

let result = calculateSum(5, 10);
console.log(result); // Output: 15
console.log(PI); // Output: 3.14159

DefinitelyTyped and @types

Both DefinitelyTyped and @types play crucial roles in providing TypeScript declaration files for existing JavaScript libraries/code.

DefinitelyTyped

A community-driven project that aims to provide high-quality TypeScript declaration files for popular JavaScript libraries. It serves as a repository of declaration files maintained by the TypeScript community.

@types

The @types scope is a convention used in the npm ecosystem to publish TypeScript declaration files for JS libraries. It is closely related to the DefinitelyTyped project.

With @types, developers can install declaration files for a specific JS library using npm. (We will provide an example in the upcoming section)

Create Declaration Files

Manually Created Declaration Files

You can manually create declaration files for JS libraries that don’t provide their own declaration files. This allows you to write type definitions and use the library in your TypeScript code with type safety.

In addition to manual creation, when using the TypeScript Compiler, you have the option to automatically generate declaration files by either —

  • specifying the --declaration flag, when invoking the tsc command:
    tsc --declaration (you can also add a specific file at the end, e.g.,app.ts)
  • or configure it in the tsconfig.json file:
{
"compilerOptions": {
"declaration": true
}
}

By doing so, tsc will generate a declaration file (app.d.ts) along with the compiled JS file (app.js). The declaration file will contain the necessary type information for the entities defined in your TypeScript code.

Declaration Files from Package Managers

Many popular JavaScript libraries, such as React, jQuery, and lodash, have official or community-contributed declaration files available. These declaration files provide type information for the library’s API, allowing seamless integration with TypeScript.

For example, if you’re using npm as your package manager, you can search for declaration files using @types. These are packages specifically created to provide TypeScript declaration files for popular JS libraries. To illustrate, the command below installs the TypeScript declaration files for React:

npm install @types/react

Once the declaration files are installed, you can start using the JS library in your TypeScript code.

It’s important to note that not all JavaScript libraries have corresponding @types packages. In such cases, you might need to manually create declaration files or search for external sources like DefinitelyTyped to find the necessary type definitions.

Write Declaration Files

Writing declaration files can be a bit challenging, especially when dealing with complex JavaScript codebases. It requires a good understanding of TypeScript’s type system, and a deep understanding of the code you’re targeting.

However, despite the challenges, the effort is worthwhile as it brings significant benefits. With that in mind, here’s an overview of the process for writing declaration files:

  1. Decide which JS code you want to create a declaration file for. It could be an external library, a specific module, or your own code.
  2. Familiarize yourself with the code you’re targeting. Understand the functions, classes, variables, and their expected behavior and usage.
  3. Start writing your declaration file by declaring the types for the entities present in the JS code. This includes functions, classes, variables, interfaces, and namespaces. Use TS syntax to define the types and their signatures.
  4. Decide whether your declaration file represents a module or global declarations. When deciding whether to use a module or global declaration file, consider the context and how the JS code is designed.
  5. Use the declare keyword to indicate that you’re providing type information for entities defined outside the TypeScript file.
  6. If your declaration file represents a module and you want to export types, use the export keyword to make them accessible for import in other TypeScript files that use the module.

Examples:

  • You can organize the declaration file as a module, encapsulating the declarations within a module block using the declare module syntax:
// myLibrary.d.ts example

declare module 'myLibrary' {
export function myFunction(): void;
export class MyClass {
constructor();
doSomething(): void;
}
}

// TS files can import and use these declarations using the import statement
  • Or, if you need to provide type information for a global object or a library that exposes entities in the global scope, you can create a global declaration file where you directly declare the types without wrapping them in a module:
// global.d.ts (global declarations example)

declare function myGlobalFunction(): void;
declare const myGlobalVariable: number;

// TS files can directly use these declarations without any import statements

7. When referencing types from other declaration files or libraries, you can utilize import or /// <reference> directives to access the required types. In the upcoming section, we’ll provide examples to illustrate this concept.

8. Test your declaration file by using it in your TS code. Ensure that the type checking and autocompletion work as expected. Make necessary refinements based on feedback or changes in the JS code.

9. Add comments and documentation to your declaration file to explain the usage and purpose of the types.

10. If relevant, share your declaration files with the community by publishing them or contributing them to existing repositories like DefinitelyTyped.

Use Declaration Files

Once the declaration files are generated, they can be used by consumers of your code. Consumers can reference the declaration files in their own projects to gain access to the type information provided by your TS code.

First example

Let’s explore how to reference types from other declaration files or libraries using import and /// <reference> directives.

Assuming you have a declaration file for a module named “myModule” that relies on types from an external library called “externalLibrary”. Here’s how you can reference the types:

  • Use import statements (if your TS version supports ES modules).
// myModule.d.ts

import { SomeType } from 'externalLibrary';

declare module 'myModule' {
export function myFunction(value: SomeType): void;
}
  • Use the /// <reference> directive to reference the necessary types (if you’re working with older TS versions or using triple-slash directives).
// myModule.d.ts

/// <reference types="externalLibrary" />

declare module 'myModule' {
export function myFunction(value: SomeType): void;
}

In this example, the directive /// <reference types=”externalLibrary” /> informs the TS compiler to include the type definitions from the ‘externalLibrary’ for use within the ‘myModule’ module.

Second example

Below we’ll examine the use of DefinitelyTyped.
When using DefinitelyTyped, you can access a specific type from a JS library by following these steps:

  • Assuming you want to use the EventEmitter type from the EventEmitter3 library.
  1. Search for “EventEmitter3” in DefinitelyTyped repository.
  2. Find the package and navigate to the “types” folder.
  3. Open the “EventEmitter3.d.ts” file or a relevant file containing the EventEmitter type definition.
// Copy the type definition
type EventEmitter = import('EventEmitter3').EventEmitter;

// Use the type in your TypeScript code
const eventEmitter: EventEmitter = new EventEmitter();

// Additional code using the EventEmitter instance
eventEmitter.on('event', () => {
console.log('Event emitted');
});
eventEmitter.emit('event');

Augmenting Modules with Declarations

Augmenting modules with declarations provides a way to extend existing modules without modifying their source code directly. This allows to provide additional type information, add new functions or variables, or enhance the existing module declarations.

Augmenting external modules

When augmenting an external module, you need to use the declare module syntax to extend the module’s declaration:

// myModule-augmentations.d.ts

declare module 'externalModule' { // augment the 'externalModule'
export function additionalFunction(): void;
}

Inside the module block, we can add new types, functions, or variables that extend the original module’s declaration. In this case, we add a new additionalFunction() to the externalModule.

This augmentation file should be placed in a location where it will be included in the TS compilation. To do this, follow these steps:

  • Locate the files or include section in the tsconfig.json file. This section specifies the list of files to include in the compilation.
  • Add the relative path of your declaration file to the section mentioned above, making sure it is relative to the tsconfig.json file.
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"outDir": "dist"
},
"include": ["src/**/*.ts", "myModule-augmentations.d.ts"]
}

Augmenting Local Modules

You can also augment local modules defined in your own codebase. For local module augmentations, the augmentation file should be included or imported in a TS file where the original module is used. This ensures that the augmentations take effect during the compilation.

Here’s how you can ensure the augmentations take effect:

// Original Module File: myLocalModule.ts

export interface MyInterface {
name: string;
age: number;
}

export function myFunction(): void {
// Code here...
}
// Augmenation File: myLocalModule-augmentations.ts

declare module './myLocalModule' {
export interface MyInterface {
address: string;
}

export function myExtendedFunction(): void;
}

export {}; // Ensure the file is treated as a module

In this example, we use the declare module syntax to augment the ‘./myLocalModule’ module with an additional property address in the MyInterface interface and a new function myExtendedFunction().

// File: main.ts

import { MyInterface, myFunction } from './myLocalModule';
import './myLocalModule-augmentations';

const obj: MyInterface = {
name: 'John',
age: 25,
address: '123 Main St',
};

myFunction();
myExtendedFunction(); // This function is available due to augmentation

We import the original module members MyInterface and myFunction from ‘./myLocalModule’. We also import the ‘myLocalModule-augmentations.ts’ file.
By including the augmentation file in the TS file where the original module is used, the augmentations are applied, and the additional types and functions become available.

Wrap-up

And that concludes our discussion on declaration files in TypeScript! We’ve explored their definition and demonstrated how to use them effectively.

I hope you found this article informative and enjoyable. 🧩
Stay tuned for more articles on advanced TypeScript features in the future, as there’s plenty more to discover! In the meantime, feel free to check out my other articles for additional helpful content.

By buying me a virtual croissant on Buy Me a Coffee, you can directly support my creative journey. Your contribution helps me continue creating high-quality content. Thank you for your support!

--

--

Writer for

🍴🛌🏻 👩🏻‍💻 🔁 Front End Web Developer | Troubleshooter | In-depth Tech Writer | 🦉📚 Duolingo Streak Master | ⚛️ React | 🗣️🤝 Interviews Prep