How to create custom global interfaces in TypeScript

Updated: January 8, 2024 By: Guest Contributor Post a comment

Introduction

TypeScript enhances JavaScript’s capabilities by allowing developers to use static typing and other features that facilitate large-scale applications. Among these additional features is the ability to create custom global interfaces – an invaluable tool for creating consistent and predictable type annotations across your entire code base.

Why Use Global Interfaces?

Before we dive into how to create global interfaces in TypeScript, let’s consider the rationale behind using them. Global interfaces offer a single point of truth for type definitions that you want to reuse across different parts of your project. This practice promotes DRY (Don’t Repeat Yourself) principles and helps to maintain consistency and ease the refactoring process as your code grows and evolves.

Defining a Basic Global Interface

interface GlobalError {
  code: number;
  message: string;
}

This code snippet defines a basic global interface for an error object. Now, whenever you deal with functions that might throw or handle errors, you can use this interface to ensure each error object conforms to the same structure.

Expanding Scope with Declaration Merging

In TypeScript, declaration merging is the process by which the compiler merges two or more separate declarations declared with the same name into a single definition. This feature is particularly useful for adding properties to existing global objects without altering the original implementations.

interface Window {
  appVersion: string;
}

window.appVersion = '1.0.0';

With the above addition, we’ve extended the global Window interface to include a custom appVersion property.

Modifying Global Interfaces in .d.ts Files

One common approach to managing global interfaces in TypeScript is through .d.ts files. These files are used solely for the purpose of declaring TypeScript shapes and types. Here’s an example of setting up a .d.ts file to manage your global interfaces:

// global.d.ts

declare global {
  interface ProcessEnv {
    NODE_ENV: 'development' | 'production';
    API_URL: string;
  }
}

export {};

By declaring an interface within the declare global block, we add the ProcessEnv interface to the global scope.

Ensuring Type Safety with Modules

While global interfaces are useful, it’s often better to use modules for encapsulating types that don’t need to be global. This enhances type safety and code maintainability. In the case that you do need to interface globally within a module, you can export the interface and then extend it globally using declaration merging.

// logger.ts
export interface Logger {
  log: (msg: string) => void;
}

// global.d.ts
import { Logger } from './logger';

declare global {
  var logger: Logger;
}

In this case, the Logger interface is defined within a module and then extended to the global scope. Be mindful of name collisions when extending interfaces globally.

Advanced Techniques: Conditional Types and Namespaces

TypeScript’s conditional types and namespaces can feature prominently in advanced use of global interfaces. These can be particularly useful when you need to tailor interfaces to the specific shape of data at runtime, or when you need to encapsulate a group of related interfaces or types.

declare namespace MyApp {
  type Validated = T extends { isValid: true } ? T : never;

  interface User {
    name: string;
    age: number;
    isValid: boolean;
  }
}

function processUser(user: T): MyApp.Validated {
  if ('isValid' in user && user.isValid) {
    return user;
  }
  throw new Error('Invalid user');
}

This example uses a conditional type within a namespace to represent users that have been validated.

Linting and Best Practices

When working with global interfaces, linting and following best practices become crucial to maintaining a clean and reliable code base. Tools like TSLint or ESLint with TypeScript support can help you enforce naming conventions, prevent duplicate declarations, and encourage best practices concerning global type declarations.

Conclusion

In conclusion, utilizing global interfaces in TypeScript can lead to more maintainable and understandable code, especially in large codebases with many developers. By following the practices outlined above, you’ll be able to leverage TypeScript’s powerful type system to create clear and consistent type definitions that can be shared across your entire project.