# Type-Safe Domain Models with Interface Augmentation in TypeScript

til · 2025-08-07 · #cube-records

While working on [`@general-dexterity/cube-records`](https://gnldxt.link/cube-records), I needed a way to define[^1] my domain-specific [Cube](https://cube.dev/product/cube-core) 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:

```typescript
// 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:

```typescript
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:

```typescript
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:

```typescript
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.

[^1]: Or use codegen with [another library](https://gnldxt.link/cube-records-codegen).
[^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.
