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.tsBarrel 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.