Home Posts Notes About

Type-Safe Domain Models with Interface Augmentation in TypeScript

August 7th, 2025 · #typescript #cube-records

While working on @general-dexterity/cube-records, I needed a way to define[1] my domain-specific Cube models in a way that would provide type-safe autocompletion. The solution was ended up pretty simple: global interface augmentation.

The Problem

The lib defines an empty global interface that users can augment with their own cube definitions. However, after publishing, I discovered that tsup was optimizing the empty interface into a type alias during the build:

// What we write
export interface CubeRecordMap {}

// What tsup outputs with dts: true
export type CubeRecordMap = {}

// Now augmentation fails
declare global {
  interface CubeRecordMap { // Error: can't augment a type alias
    users: { /* ... */ }
  }
}

The Solution

Adding a dummy property prevents this optimization:

export interface CubeRecordMap {
  __empty: {
    measures: {};
    dimensions: {};
    joins: [];
  };
}

That __empty property isn't arbitrary - it ensures the interface remains augmentable through the entire build pipeline[2].

Implementation

Users can now extend the interface in their projects:

declare global {
  interface CubeRecordMap {
    users: {
      measures: { count: { type: number } };
      dimensions: {
        id: { type: string };
        email: { type: string };
      };
      joins: readonly ['orders'];
    };
    // ... other models and views
  }
}

The library extracts type-safe cube names and fields:

type CubeRecordName = keyof CubeRecordMap;
type CubeRecordMeasure<T extends CubeRecordName> =
  keyof CubeRecordMap[T]['measures'] & string;

Trade-offs

Benefits:

  • Interface stays augmentable through the build pipeline
  • Zero runtime overhead - types exist only at compile time
  • Incremental cube definitions with immediate IDE support

Drawbacks:

  • The interface includes a dummy property
  • Requires some documentation to explain the pattern

Notes

This isn't just for my personal benefits[3], this pattern provides domain-specific autocompletion without runtime cost. Instead of searching through Cube models for field names, TypeScript provides instant feedback as you type.


Footnotes

  1. Or use codegen with another library. ↩︎

  2. This is specifically a build tool optimization when generating declaration files, not a TypeScript language limitation. ↩︎

  3. Even though it was pretty fun to try to figure this out. ↩︎

Mastodon