<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aarav Pradhan</title>
    <description>The latest articles on DEV Community by Aarav Pradhan (@aarav_pradhan).</description>
    <link>https://dev.to/aarav_pradhan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3356274%2F0312d898-349f-4374-9b97-db3be7580f50.jpg</url>
      <title>DEV Community: Aarav Pradhan</title>
      <link>https://dev.to/aarav_pradhan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aarav_pradhan"/>
    <language>en</language>
    <item>
      <title>Building Type-Safe Entity Management with TypeScript Discriminated Unions</title>
      <dc:creator>Aarav Pradhan</dc:creator>
      <pubDate>Tue, 15 Jul 2025 08:27:24 +0000</pubDate>
      <link>https://dev.to/aarav_pradhan/building-type-safe-entity-management-with-typescript-discriminated-unions-3o6g</link>
      <guid>https://dev.to/aarav_pradhan/building-type-safe-entity-management-with-typescript-discriminated-unions-3o6g</guid>
      <description>&lt;p&gt;When building complex applications, we often encounter entities that come in different "flavors" - think products with different categories, users with various roles, or documents with distinct types. The challenge? How do we model these variants in TypeScript while maintaining type safety and developer experience?&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Common Anti-Pattern&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Most developers start with what seems like the obvious solution - one interface with optional fields for everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;interface IEntity {
    id: string;
    title: string;
    type: 'product' | 'service' | 'subscription';

    // Product-specific fields
    weight?: number;
    dimensions?: { width: number; height: number; depth: number };

    // Service-specific fields
    duration?: number;
    location?: string;

    // Subscription-specific fields
    billingCycle?: 'monthly' | 'yearly';
    features?: string[];

    // ... and 20+ more optional fields
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While this works functionally, it creates several problems:&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Problems with the Optional Fields Approach&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No Type Safety:&lt;/strong&gt; Nothing prevents you from setting weight on a service or duration on a product&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Poor Developer Experience:&lt;/strong&gt; IntelliSense shows all fields for all types, making it confusing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime Errors:&lt;/strong&gt; Easy to access undefined fields or set invalid combinations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintenance Nightmare:&lt;/strong&gt; Adding new types means touching one massive interface&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unclear Requirements:&lt;/strong&gt; Hard to tell which fields are actually required for each type&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Solution: Discriminated Unions&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Discriminated unions (also called tagged unions) solve this elegantly by creating separate interfaces for each variant, then combining them with a union type.&lt;/p&gt;

&lt;p&gt;Here's how to refactor the above example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Base interface with common properties
interface IBaseEntity {
    id: string;
    title: string;
    description: string;
    createdAt: Date;
    updatedAt: Date;
}

// Product-specific interface
interface IProduct extends IBaseEntity {
    type: 'product';
    weight: number;
    dimensions: {
        width: number;
        height: number;
        depth: number;
    };
    sku: string;
    inventory: number;
}

// Service-specific interface
interface IService extends IBaseEntity {
    type: 'service';
    duration: number;
    location: string;
    bookingRequired: boolean;
    availability: string[];
}

// Subscription-specific interface
interface ISubscription extends IBaseEntity {
    type: 'subscription';
    billingCycle: 'monthly' | 'yearly';
    features: string[];
    trialPeriod: number;
    maxUsers: number;
}

// Union type combining all variants
export type IEntity = IProduct | IService | ISubscription;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Type Guards for Runtime Safety&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;To work with these unions safely at runtime, create type guard functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export const isProduct = (entity: IEntity): entity is IProduct =&amp;gt; {
    return entity.type === 'product';
};

export const isService = (entity: IEntity): entity is IService =&amp;gt; {
    return entity.type === 'service';
};

export const isSubscription = (entity: IEntity): entity is ISubscription =&amp;gt; {
    return entity.type === 'subscription';
};

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Usage in Practice&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Now you can work with entities in a type-safe manner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function processEntity(entity: IEntity) {
    // Common properties are always available
    console.log(entity.title);

    // Type-specific logic with full type safety
    if (isProduct(entity)) {
        console.log(`Weight: ${entity.weight}kg`);
        console.log(`SKU: ${entity.sku}`);
        // entity.duration; // ❌ TypeScript error!
    } else if (isService(entity)) {
        console.log(`Duration: ${entity.duration} minutes`);
        console.log(`Location: ${entity.location}`);
        // entity.weight; // ❌ TypeScript error!
    } else if (isSubscription(entity)) {
        console.log(`Billing: ${entity.billingCycle}`);
        console.log(`Features: ${entity.features.join(', ')}`);
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Creating Clean Input Types&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;One challenge with extending Document (for Mongoose) or similar ORM interfaces is that they include methods you don't want in your input types. Here's how to create clean input types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Document } from 'mongoose';

// Helper type that removes Document methods for clean inputs
export type CreateEntityInput&amp;lt;T extends IEntity['type']&amp;gt; = 
    T extends 'product' ? Omit&amp;lt;IProduct, keyof Document&amp;gt; :
    T extends 'service' ? Omit&amp;lt;IService, keyof Document&amp;gt; :
    T extends 'subscription' ? Omit&amp;lt;ISubscription, keyof Document&amp;gt; :
    never;

// Usage in API controllers
export const createEntity = async (req: Request, res: Response) =&amp;gt; {
    const { type } = req.body;

    // TypeScript knows exactly what fields are required based on type
    const entityData: CreateEntityInput&amp;lt;typeof type&amp;gt; = req.body;

    const entity = new Entity(entityData);
    await entity.save();
};

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;File Organization&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Keep your types organized for maintainability:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
├── types/
│   ├── index.ts           # Export all types
│   ├── entity.types.ts    # Entity-related types
│   ├── user.types.ts      # User types
│   └── common.types.ts    # Shared types
├── models/
│   └── Entity.ts
├── controllers/
└── services/

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Advanced Pattern: Generic Functions&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;You can create generic functions that work with specific entity types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function calculateShippingCost&amp;lt;T extends IEntity&amp;gt;(
    entity: T
): T extends IProduct ? number : never {
    if (isProduct(entity)) {
        const volume = entity.dimensions.width * entity.dimensions.height * entity.dimensions.depth;
        return (entity.weight * 0.1 + volume * 0.05) as any;
    }
    throw new Error('Shipping cost only applicable to products');
}

// Usage
const product: IProduct = { /* ... */ };
const cost = calculateShippingCost(product); // Returns number

const service: IService = { /* ... */ };
const cost2 = calculateShippingCost(service); // TypeScript error!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Benefits in Real-World Applications&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Compile-Time Safety&lt;/strong&gt;&lt;br&gt;
// This will fail at compile time, not runtime&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function processProduct(product: IProduct) {
    console.log(product.duration); // ❌ TypeScript error
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Better IntelliSense&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you type entity. in your IDE, you'll only see relevant fields based on the discriminated type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Easier Refactoring&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Need to add a new field to products? Just modify the IProduct interface. TypeScript will show you everywhere that needs updating.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Self-Documenting Code&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The type system becomes documentation - it's immediately clear what fields are available for each entity type.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;When to Use This Pattern&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Discriminated unions are perfect for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Product catalogs&lt;/strong&gt; with different product types&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User systems&lt;/strong&gt; with different roles (admin, customer, vendor)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document management&lt;/strong&gt; with different document types&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payment methods&lt;/strong&gt; (credit card, PayPal, bank transfer)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notification systems&lt;/strong&gt; with different notification types&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content management&lt;/strong&gt; with different content types&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Common Pitfalls to Avoid&lt;/strong&gt;
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Don't make the discriminator optional&lt;/strong&gt;: The type field must be required&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use literal types&lt;/strong&gt;: type: 'product' not type: string&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't mix concerns&lt;/strong&gt;: Keep type-specific logic in the appropriate interfaces&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consider maintenance&lt;/strong&gt;: Don't create too many variants if they're very similar&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Conclusion&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Discriminated unions transform how you handle entity variants in TypeScript. They provide compile-time safety, better developer experience, and more maintainable code. While it requires a bit more upfront setup than the "optional fields everywhere" approach, the long-term benefits are substantial.&lt;/p&gt;

&lt;p&gt;The next time you find yourself with an interface full of optional fields, consider whether discriminated unions might be a better fit. Your future self (and your teammates) will thank you for the improved type safety and clarity.&lt;/p&gt;




&lt;p&gt;Have you used discriminated unions in your projects? What challenges did you face, and how did you solve them? Share your experience in the comments below!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>programming</category>
      <category>development</category>
    </item>
  </channel>
</rss>
