25

Module 25: Type-Safe Module Federation (Senior/Enterprise)

Chapter 25 • Advanced

40 min

Type-Safe Module Federation (Senior/Enterprise)

The Real Problem

Scenario: Products team changes CartItem interface. Checkout app breaks at runtime. TypeScript didn't catch it because remote types aren't available.

The Question: How do I maintain type safety across micro frontends when remotes can change and TypeScript can't see remote code?

This is type safety at scale.


The Production Reality

Type Safety Challenges

  1. Remote Types Unavailable - TypeScript can't see remote code
  2. Runtime Type Mismatches - Types don't match at runtime
  3. Version Changes - Remote types change, local types don't
  4. Contract Evolution - Types change, but need backward compatibility
  5. Shared Types - Types shared across apps

The Question: How do we solve each challenge?


Strategy 1: Shared Type Definitions Package

The Pattern

Create a shared types package. All apps consume it.

Step 1: Create Types Package

bash.js
# Create types package
mkdir shared-types
cd shared-types
npm init -y

# Structure
shared-types/
├── src/
│   ├── cart.types.ts
│   ├── product.types.ts
│   ├── user.types.ts
│   └── index.ts
└── package.json

Step 2: Define Shared Types

typescript.js
// shared-types/src/cart.types.ts
export interface CartItem {
  id: string
  productId: string
  name: string
  price: number
  quantity: number
  version: '1.0.0'
}

export interface Cart {
  items: CartItem[]
  total: number
  version: '1.0.0'
}
typescript.js
// shared-types/src/product.types.ts
export interface Product {
  id: string
  name: string
  description: string
  price: number
  imageUrl: string
  category: string
}

export interface ProductListResponse {
  products: Product[]
  total: number
  page: number
}
typescript.js
// shared-types/src/index.ts
export * from './cart.types'
export * from './product.types'
export * from './user.types'

Step 3: Consume in Apps

typescript.js
// products-app/package.json
{
  "dependencies": {
    "@company/shared-types": "^1.0.0"
  }
}

// products-app/src/app/cart/cart.service.ts
import { CartItem, Cart } from '@company/shared-types'

@Injectable({ providedIn: 'root' })
export class CartService {
  private items: CartItem[] = []
  
  addItem(item: CartItem) {
    this.items.push(item)
  }
  
  getCart(): Cart {
    return {
      items: this.items,
      total: this.items.reduce((sum, item) => sum + item.price * item.quantity, 0),
      version: '1.0.0'
    }
  }
}
typescript.js
// checkout-app/src/app/cart/cart.service.ts
import { CartItem, Cart } from '@company/shared-types'

@Injectable({ providedIn: 'root' })
export class CheckoutCartService {
  private items: CartItem[] = []
  
  loadCart(cart: Cart) {
    // Type-safe: TypeScript knows Cart structure
    this.items = cart.items
  }
}

Strategy 2: Type Definitions for Remotes

The Pattern

Generate type definitions for remote modules.

Step 1: Define Remote Module Types

typescript.js
// host-app/src/types/remotes.d.ts
declare module 'products/ProductsModule' {
  import { Type } from '@angular/core'
  
  export class ProductsModule {}
  export class ProductsComponent {}
  export class ProductDetailComponent {}
}

declare module 'checkout/CheckoutModule' {
  import { Type } from '@angular/core'
  
  export class CheckoutModule {}
  export class CheckoutComponent {}
  export class CartComponent {}
}

Step 2: Use in Host

typescript.js
// host-app/src/app/app-routing.module.ts
import { ProductsModule } from 'products/ProductsModule' // Type-safe!
import { CheckoutModule } from 'checkout/CheckoutModule' // Type-safe!

const routes: Routes = [
  {
    path: 'products',
    loadChildren: () => import('products/ProductsModule')
      .then(m => m.ProductsModule) // TypeScript knows this exists
  },
  {
    path: 'checkout',
    loadChildren: () => import('checkout/CheckoutModule')
      .then(m => m.CheckoutModule) // TypeScript knows this exists
  }
]

Strategy 3: Runtime Type Validation

The Problem

Types match at compile time, but not at runtime.

Solution: Runtime Type Guards

typescript.js
// shared-types/src/type-guards.ts
import { CartItem } from './cart.types'

export function isCartItem(value: any): value is CartItem {
  return (
    typeof value === 'object' &&
    value !== null &&
    typeof value.id === 'string' &&
    typeof value.productId === 'string' &&
    typeof value.name === 'string' &&
    typeof value.price === 'number' &&
    typeof value.quantity === 'number' &&
    value.version === '1.0.0'
  )
}

export function isCart(value: any): value is Cart {
  return (
    typeof value === 'object' &&
    value !== null &&
    Array.isArray(value.items) &&
    value.items.every(isCartItem) &&
    typeof value.total === 'number' &&
    value.version === '1.0.0'
  )
}

Use in Code

typescript.js
// checkout-app/src/app/cart/cart.service.ts
loadCart(cart: unknown) {
  if (isCart(cart)) {
    // TypeScript knows cart is Cart type
    this.items = cart.items
  } else {
    console.error('Invalid cart structure:', cart)
    throw new Error('Invalid cart')
  }
}

Strategy 4: Versioned Types

The Problem

CartItem changes. Old Checkout breaks.

Solution: Versioned Type Definitions

typescript.js
// shared-types/src/cart.types.ts
// Version 1.0.0
export interface CartItemV1 {
  id: string
  productId: string
  name: string
  price: number
  quantity: number
  version: '1.0.0'
}

// Version 2.0.0 (additive)
export interface CartItemV2 extends CartItemV1 {
  discount?: number
  appliedPromo?: string
  version: '2.0.0'
}

// Union type for backward compatibility
export type CartItem = CartItemV1 | CartItemV2

Use with Type Guards

typescript.js
// checkout-app/src/app/cart/cart.service.ts
handleCartItem(item: CartItem) {
  if (item.version === '1.0.0') {
    // Handle v1
    const v1Item = item as CartItemV1
    // Use v1Item
  } else if (item.version === '2.0.0') {
    // Handle v2
    const v2Item = item as CartItemV2
    // Use v2Item.discount, v2Item.appliedPromo
  }
}

Strategy 5: Type Generation from APIs

The Pattern

Generate TypeScript types from API schemas.

Step 1: API Schema (OpenAPI/Swagger)

yaml.js
# api-schema.yaml
components:
  schemas:
    CartItem:
      type: object
      properties:
        id:
          type: string
        productId:
          type: string
        name:
          type: string
        price:
          type: number
        quantity:
          type: number
      required:
        - id
        - productId
        - name
        - price
        - quantity

Step 2: Generate Types

bash.js
# Install type generator
npm install --save-dev openapi-typescript

# Generate types
npx openapi-typescript api-schema.yaml -o src/types/api.d.ts

Step 3: Use Generated Types

typescript.js
// products-app/src/app/api/products.api.ts
import { CartItem } from '@/types/api'

@Injectable({ providedIn: 'root' })
export class ProductsApiService {
  constructor(private http: HttpClient) {}
  
  addToCart(item: CartItem): Observable<CartItem> {
    return this.http.post<CartItem>('/api/cart', item)
  }
}

Strategy 6: Contract Testing with Types

The Pattern

Test that types match at runtime.

Step 1: Type Contract Tests

typescript.js
// shared-types/src/contract-tests.ts
import { CartItem, isCartItem } from './cart.types'

describe('CartItem Contract', () => {
  it('should validate CartItem structure', () => {
    const validItem: CartItem = {
      id: '1',
      productId: '2',
      name: 'Product',
      price: 10,
      quantity: 1,
      version: '1.0.0'
    }
    
    expect(isCartItem(validItem)).toBe(true)
  })
  
  it('should reject invalid CartItem', () => {
    const invalidItem = {
      id: '1',
      // Missing required fields
    }
    
    expect(isCartItem(invalidItem)).toBe(false)
  })
})

Step 2: Integration Type Tests

typescript.js
// products-app/src/app/cart/cart.service.spec.ts
import { CartItem } from '@company/shared-types'

describe('CartService Type Safety', () => {
  it('should accept CartItem type', () => {
    const service = new CartService()
    const item: CartItem = {
      id: '1',
      productId: '2',
      name: 'Product',
      price: 10,
      quantity: 1,
      version: '1.0.0'
    }
    
    // TypeScript ensures type safety
    service.addItem(item)
    expect(service.getItems()).toContain(item)
  })
})

Strategy 7: Type-Safe Event Contracts

The Pattern

Type-safe event definitions.

Step 1: Define Event Types

typescript.js
// shared-types/src/events.types.ts
export interface EventContract<T = any> {
  event: string
  version: string
  data: T
  timestamp: number
}

export interface CartItemAddedEvent extends EventContract<{
  itemId: string
  productId: string
  quantity: number
  price: number
}> {
  event: 'cart:item-added'
  version: '1.0.0'
}

export interface CartUpdatedEvent extends EventContract<{
  items: CartItem[]
  total: number
}> {
  event: 'cart:updated'
  version: '1.0.0'
}

export type AppEvent = CartItemAddedEvent | CartUpdatedEvent

Step 2: Type-Safe Event Bus

typescript.js
// shared-types/src/event-bus.types.ts
export class TypedEventBus {
  private subject = new Subject<AppEvent>()
  
  emit<T extends AppEvent>(event: T): void {
    this.subject.next(event)
  }
  
  on<T extends AppEvent['event']>(
    eventName: T
  ): Observable<Extract<AppEvent, { event: T }>> {
    return this.subject.pipe(
      filter((e): e is Extract<AppEvent, { event: T }> => 
        e.event === eventName
      )
    )
  }
}

Step 3: Use Type-Safe Events

typescript.js
// products-app/src/app/cart/cart.service.ts
emitCartItemAdded(item: CartItem) {
  const event: CartItemAddedEvent = {
    event: 'cart:item-added',
    version: '1.0.0',
    data: {
      itemId: item.id,
      productId: item.productId,
      quantity: item.quantity,
      price: item.price
    },
    timestamp: Date.now()
  }
  
  this.eventBus.emit(event) // Type-safe!
}
typescript.js
// checkout-app/src/app/cart/cart.service.ts
constructor(private eventBus: TypedEventBus) {
  // TypeScript knows the event structure
  this.eventBus.on('cart:item-added').subscribe(event => {
    // event is CartItemAddedEvent
    // TypeScript knows event.data structure
    const { itemId, productId, quantity, price } = event.data
  })
}

Production Checklist

  • [ ] Shared types package created
  • [ ] Remote module types defined
  • [ ] Runtime type guards implemented
  • [ ] Versioned types for backward compatibility
  • [ ] Type generation from APIs (if applicable)
  • [ ] Contract tests for types
  • [ ] Type-safe event contracts
  • [ ] Documentation for type usage

Key Takeaways

  1. Shared types package - Single source of truth
  2. Runtime validation - Types don't guarantee runtime safety
  3. Versioned types - Backward compatibility
  4. Type generation - Keep types in sync with APIs
  5. Type-safe events - Contracts for communication

Remember: Type safety in micro frontends requires discipline and tooling.

Congratulations! You've completed the entire Micro Frontend Architecture course. You now understand how to build, test, secure, and maintain production-grade micro frontend systems at scale.

You've crossed the line from "I've used Module Federation" to "I've shipped and maintained it in production."