Back to Blog
Lesson 37 of the TypeScript: Typing JavaScript with Confidence course
TypeScriptAugust 24, 20264 min read

Type Declaration Files (.d.ts): Bridging JS and TypeScript

Learn how to create .d.ts declaration files to add type safety to legacy JavaScript modules and bridge the gap between untyped code and TypeScript.

TypeScriptDeclaration Files.d.tsInteropModulesJavaScript
Close-up of colorful love padlocks on a cable, symbolizing eternal love and commitment.

Previously in this course, we covered Using DefinitelyTyped: Mastering External Type Definitions to import existing types for popular libraries. But what happens when you have a custom, internal JavaScript utility library that lacks types? In this lesson, we'll learn how to create your own .d.ts declaration files to describe that code to the TypeScript compiler, effectively bridging the gap between JavaScript and TypeScript environments.

Understanding Declaration Files (.d.ts)

A .d.ts file is a "declaration" file. It contains zero executable code; instead, it provides the TypeScript compiler with a map of your JavaScript files. Think of it as a contract: it tells TypeScript what functions, variables, and modules exist and what their shapes are, allowing the compiler to perform static analysis on your JavaScript code without requiring you to rewrite the source.

When you use a library written in TypeScript, the compiler automatically generates these definitions for you. When you have a plain .js file, TypeScript has no visibility into its contents. By writing a .d.ts file, you are providing the necessary "metadata" so that you can keep using that library while enjoying full autocomplete and type safety in your IDE.

Declaring Modules for Interop

Often, you'll encounter a legacy module that isn't exported in a way that TypeScript likes, or perhaps it's a global script. To tell TypeScript about a module, we use the declare module syntax.

Imagine we have an existing internal JS utility file called logger.js in our project:

JAVASCRIPT
// logger.js
export function logTask(task) {
  console.log(CE9178">`Task: ${task.title}`);
}

If we try to import this in a .ts file, TypeScript will complain that it doesn't know the structure of task. We can bridge this by creating a logger.d.ts file in the same directory:

TYPESCRIPT
// logger.d.ts
declare module "./logger" {
  export interface Task {
    title: string;
    completed: boolean;
  }

  export function logTask(task: Task): void;
}

By placing this file in your project, the TypeScript compiler now sees the logTask function and its required Task interface whenever you import logger.js.

Worked Example: Typing the Task API Client

In our running project, let's assume we have a legacy api-config.js file that holds constants. We need to add types to this so our Building the Task API Client: Setup logic can safely consume it.

  1. The JS File:

    JAVASCRIPT
    // api-config.js
    export const BASE_URL = "https://api.example.com";
    export const TIMEOUT = 5000;
  2. The Declaration File: Create api-config.d.ts:

    TYPESCRIPT
    declare module "./api-config" {
      export const BASE_URL: string;
      export const TIMEOUT: number;
    }

Now, when you import these in your main client code, TypeScript knows exactly what they are. This keeps our legacy JS integration strictly typed without needing a build-time migration of that specific file.

Hands-on Exercise

  1. Create a file named legacy-utils.js with a function calculateTotal(items).
  2. Create a corresponding legacy-utils.d.ts file.
  3. Define an interface for the item and declare the function signature.
  4. Import calculateTotal into a .ts file and verify that your editor shows the correct types on hover.

Common Pitfalls

  • File Naming: Ensure your .d.ts file name matches the .js file name (e.g., utils.js -> utils.d.ts). If the names don't match, TypeScript won't automatically associate the definitions with the import.
  • Shadowing: Don't put logic code inside a .d.ts file. The compiler will ignore it, but it creates confusion for maintainers. It is strictly for type signatures.
  • Module Resolution: If you are dealing with global variables rather than modules, use declare var or declare function instead of declare module.

FAQ

Q: Do I need a .d.ts file for every JS file? A: No. Only create them for files where you want type safety. If you don't care about the types for a specific utility, you can leave it as any.

Q: Can I use declaration merging here? A: Yes, you can use Mastering TypeScript Declaration Merging for Extensible Code to extend types even for legacy JS modules if they have global interfaces.

Q: Should I commit .d.ts files? A: Yes. They are part of your source code and are essential for your development team's experience and the compiler's performance.

Recap

Declaration files are your bridge between the untyped past and the type-safe future. By creating .d.ts files, you define the "shape" of your JavaScript, allowing TypeScript to catch errors in your legacy code path just as effectively as in your modern TS files.

Up next: We'll dive into how to enforce strictness in your project by Enabling Strict Null Checks.

Similar Posts