Skip to main content

TypeScript Declaration Pitfalls: 10 Costly Decisions to Avoid

· 4 min read
Max Kaido
Architect

When building a TypeScript project, type declarations are your first line of defense against bugs and maintainability issues. However, certain type declaration patterns can actually work against you, creating technical debt and making your codebase harder to maintain. Let's explore 10 specific anti-patterns we've identified in our analysis of real-world TypeScript projects.

The Cost of Poor Type Declarations

Before diving into specific issues, it's important to understand why type declarations matter so much:

  • They serve as living documentation
  • They enable IDE features like auto-completion and refactoring
  • They catch bugs before runtime
  • They make code more maintainable and self-documenting

When type declarations are done poorly, you lose these benefits and potentially create more problems than you solve.

10 Type Declaration Anti-Patterns to Avoid

1. The "declare module" Trap

Severity: Critical 🔴 Impact: 9/10 Difficulty to Fix: 8/10

// DON'T DO THIS
declare module 'apps/my-app/src/app.service' {
export class AppService {
// ...
}
}

// DO THIS INSTEAD
// app.service.ts
export class AppService {
// ...
}

This is perhaps the most severe anti-pattern we've encountered. Using declare module for every file:

  • Breaks TypeScript's module resolution
  • Makes refactoring nearly impossible
  • Prevents IDE features from working properly
  • Creates a parallel "virtual" type system

The Fix: Use proper ES modules with explicit imports/exports. Structure your code as real modules that TypeScript can understand and analyze.

2. The "God Service" Anti-Pattern

Severity: High 🔴 Impact: 8/10 Difficulty to Fix: 7/10

// DON'T DO THIS
export class AIService {
private getRandomModel() {
/* ... */
}
private calculatePriceAndLimits() {
/* ... */
}
async generateDailyInsight() {
/* ... */
}
async generateCardTeaching() {
/* ... */
}
async compressText() {
/* ... */
}
}

// DO THIS INSTEAD
export class AIModelSelector {
selectModel(requirements: ModelRequirements): AIModel {
/* ... */
}
}

export class AIPricingService {
calculateCost(modelType: string, tokens: number): Price {
/* ... */
}
}

export class AIGenerationService {
generateText(prompt: string, model: AIModel): Promise<string> {
/* ... */
}
}

Lumping multiple responsibilities into a single service creates a maintenance nightmare and makes type declarations overly complex.

3. Mixed Domain and Infrastructure Types

Severity: High 🔴 Impact: 8/10 Difficulty to Fix: 6/10

// DON'T DO THIS
export class ReadingService {
constructor(
private em: EntityManager,
private deckService: DeckService,
private aiService: AIService,
private tokenService: TokenService,
) {}

async createReading() {
// Mixed domain and DB logic
}
}

// DO THIS INSTEAD
export class ReadingDomainService {
constructor(
private repository: ReadingRepository,
private deckService: DeckService,
) {}

async createReading(reading: Reading): Promise<Reading> {
// Pure domain logic
}
}

export class ReadingRepository {
constructor(private em: EntityManager) {}

async save(reading: Reading): Promise<void> {
// Pure persistence logic
}
}

4. String Literal Overuse

Severity: Medium 🟡 Impact: 7/10 Difficulty to Fix: 4/10

// DON'T DO THIS
type SpreadType = 'three-card' | 'celtic-cross';
type ImageStyle = 'vivid' | 'natural';

// DO THIS INSTEAD
export enum SpreadType {
ThreeCard = 'three-card',
CelticCross = 'celtic-cross',
}

export enum ImageStyle {
Vivid = 'vivid',
Natural = 'natural',
}

String literals scattered throughout the code create maintenance headaches and make refactoring risky.

5. Duplicated Utility Types

Severity: Medium 🟡 Impact: 6/10 Difficulty to Fix: 5/10

// DON'T DO THIS
// In multiple files:
type Environment = 'production' | 'development' | 'test';
type FormatOptions = { bold?: boolean; italic?: boolean };

// DO THIS INSTEAD
// shared/types/common.ts
export enum Environment {
Production = 'production',
Development = 'development',
Test = 'test',
}

export interface FormatOptions {
bold?: boolean;
italic?: boolean;
}

6. Domain-Infrastructure Type Coupling

Severity: Medium 🟡 Impact: 7/10 Difficulty to Fix: 6/10

// DON'T DO THIS
interface TarotReading {
blockHash: string; // TON blockchain dependency
cards: string[];
interpretation: string;
}

// DO THIS INSTEAD
interface TarotReading {
id: string;
cards: Card[];
interpretation: string;
}

interface BlockchainReadingMetadata {
readingId: string;
blockHash: string;
}

7. Pricing Logic in Domain Types

Severity: Medium 🟡 Impact: 6/10 Difficulty to Fix: 5/10

// DON'T DO THIS
interface AIResponse {
text: string;
costUsd: number;
priceTaro: number;
}

// DO THIS INSTEAD
interface AIResponse {
text: string;
metadata: AIResponseMetadata;
}

interface AIResponseMetadata {
modelUsed: string;
tokensUsed: number;
}

interface PricingInfo {
responseId: string;
costUsd: number;
priceTaro: number;
}

8. Scattered Handler Types

Severity: Low 🟢 Impact: 5/10 Difficulty to Fix: 4/10

// DON'T DO THIS
// In separate files:
type StartHandler = (ctx: Context) => Promise<void>;
type HelpHandler = (ctx: Context) => Promise<void>;

// DO THIS INSTEAD
export type CommandHandler = (ctx: Context) => Promise<void>;

export interface BotHandlers {
start: CommandHandler;
help: CommandHandler;
about: CommandHandler;
}

9. Inconsistent Domain Types

Severity: Low 🟢 Impact: 5/10 Difficulty to Fix: 4/10

// DON'T DO THIS
interface Card {
name: string;
}
type CardName = string;
type DeckCard = { key: string; name: string };

// DO THIS INSTEAD
interface Card {
id: string;
name: string;
description: string;
imageUrl: string;
}

interface Deck {
id: string;
name: string;
cards: Card[];
}

10. Queue Type Fragmentation

Severity: Low 🟢 Impact: 4/10 Difficulty to Fix: 3/10

// DON'T DO THIS
type JobName = 'tarot_reading' | 'tarot_image';
type QueueName = 'tarot' | 'image';

// DO THIS INSTEAD
export enum JobType {
TarotReading = 'tarot.reading.generate',
TarotImage = 'tarot.image.generate',
}

export interface JobData<T> {
id: string;
type: JobType;
data: T;
metadata: JobMetadata;
}

Implementation Strategy

To fix these issues effectively, follow this order:

  1. Immediate Fixes (1-2 weeks):

    • Convert declare module to proper ES modules
    • Create centralized type definitions
    • Implement enums for string literals
  2. Short-term Improvements (2-4 weeks):

    • Split large services into focused ones
    • Separate domain and infrastructure types
    • Consolidate handler types
  3. Long-term Refactoring (1-2 months):

    • Implement proper domain boundaries
    • Create comprehensive type hierarchies
    • Add thorough type documentation

Best Practices Moving Forward

  1. Single Source of Truth

    • Keep shared types in a central location
    • Use proper module imports/exports
    • Document type relationships
  2. Domain-Driven Types

    • Separate domain and infrastructure concerns
    • Use meaningful names that reflect business concepts
    • Keep types focused and single-purpose
  3. Type Safety

    • Avoid any and type assertions
    • Use strict TypeScript configuration
    • Write comprehensive type tests
  4. Maintainability

    • Document complex type relationships
    • Use TypeScript's advanced features judiciously
    • Keep type definitions close to their usage

Conclusion

Poor type declarations can significantly impact a project's maintainability and developer experience. By identifying and fixing these anti-patterns early, you can create a more maintainable and robust TypeScript codebase.

Remember:

  • Types should serve as documentation
  • Keep domain concepts clear and separate
  • Use TypeScript's features wisely
  • Maintain a single source of truth for shared types

What type declaration anti-patterns have you encountered in your projects? Share your experiences in the comments below!