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 define1 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 pipeline2.

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 benefits3, 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