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
Or use codegen with another library. ↩︎
This is specifically a build tool optimization when generating declaration files, not a TypeScript language limitation. ↩︎
Even though it was pretty fun to try to figure this out. ↩︎