Skip to main content

organizing-type-definitions

Organizing Type Definitions:

Let's illustrate these key points with TypeScript examples.

  • Modularity:

    // mathUtils.ts
    export type AddFn = (a: number, b: number) => number;

    // Using the type in another module
    // calculator.ts
    import { AddFn } from './mathUtils';

    const add: AddFn = (a, b) => a + b;
  • Declaration files:

    // jquery.d.ts
    interface JQuery {
    fadeIn(): void;
    }

    // Usage in a .ts file
    /// <reference path="jquery.d.ts" />
    $('div').fadeIn();
  • Namespaces (to be avoided in favor of modules):

    // Instead of using namespaces
    namespace MyMath {
    export function add(a: number, b: number): number {
    return a + b;
    }
    }

    // Prefer using modules
    // math.ts
    export function add(a: number, b: number): number {
    return a + b;
    }
  • Type versus interface:

    // Prefer using an interface
    interface User {
    name: string;
    age: number;
    }

    // Extending the interface
    interface Admin extends User {
    privileges: string[];
    }
  • Single Responsibility:

    // Good: Each type has a single responsibility
    type User = {
    name: string;
    age: number;
    };

    type Permission = {
    permissions: string[];
    };
  • Organize by feature:

    // File structure
    /users
    userTypes.ts
    userService.ts
    /products
    productTypes.ts
    productService.ts
  • Barrel exports:

    // index.ts in the users folder
    export * from './userTypes';
    export * from './userService';

    // Importing from another module
    import { User, fetchUsers } from '../users';
  • Avoid global types:

    // Instead of defining global
    declare global {
    type UserID = string;
    }

    // Define locally
    // types.ts
    export type UserID = string;

    // Use locally
    // user.ts
    import { UserID } from './types';

    const user: UserID = 'abc123';

Here are examples illustrating these additional TypeScript key points:

  • Shared types:

    // types/common.ts
    export type ID = string | number;

    // In different modules, you can import the shared type
    import { ID } from './types/common';
  • Naming conventions:

    // Clear and descriptive names for interface
    interface UserResponse {
    users: Array<User>;
    total: number;
    }

    // For generics
    interface Repository<T> {
    getById(id: ID): T;
    save(entity: T): void;
    }
  • Generics:

    // Generic function to return array elements or null
    function getFirst<T>(items: T[]): T | null {
    return items.length > 0 ? items[0] : null;
    }
  • Type narrowing:

    // Type guard to narrow type
    function isNumber(value: unknown): value is number {
    return typeof value === 'number';
    }

    // Usage
    const result: unknown = getResult();
    if (isNumber(result)) {
    console.log(result.toFixed(2)); // Type is now narrowed to 'number'
    }
  • Extend with care:

    // Extending native types can be confusing
    interface CustomArray<T> extends Array<T> {
    getFirstElement(): T | undefined;
    }
  • Enums and constants:

    // Using enums instead of literal types
    enum UserRole {
    Admin = 'ADMIN',
    User = 'USER',
    Guest = 'GUEST'
    }
  • Documentation:

    /**
    * Represents a user of the application.
    * @property {string} id - The unique identifier for a user.
    * @property {string} name - The name of the user.
    */
    interface User {
    id: string;
    name: string;
    }

These code samples provide a clear understanding of how to apply these TypeScript best practices. Remember that good documentation and consistent naming go a long way in maintaining a codebase, especially when dealing with complex type definitions and generics.

Understanding how to effectively organize type definitions in TypeScript is essential for maintaining a clean and manageable codebase, and for maximizing the benefit of TypeScript's powerful type system in larger projects.