Drillable Namespace Pattern
The Drillable Namespace Pattern is a module organization approach that enables dual import forms for your modules, giving users flexibility in how they access your library's functionality.
What is it?
The Drillable Namespace Pattern allows a module to be imported in two equivalent ways:
// Form 1: Named import from main package
import { Err } from '@wollybeard/kit'
// Form 2: Namespace import from subpath
import * as Err from '@wollybeard/kit/err'
Both import forms provide exactly the same API - they're just different syntactic ways to access the same functionality.
Why use it?
Benefits for Users
- Flexibility: Users can choose their preferred import style
- Bundle optimization: Subpath imports can enable better tree-shaking in some bundlers
- Explicit dependencies: Subpath imports make it clear which modules are being used
- Namespace clarity: Named imports from the main package clearly show the module structure
When to Use
Use the Drillable Namespace Pattern when:
- Your module is a cohesive namespace with multiple related functions
- Users would benefit from flexible import options
- The module represents a significant domain concept (e.g.,
Err
,Test
,Num
)
Don't use it for:
- Simple utility functions without a clear namespace identity
- Internal-only modules
- Temporary or experimental APIs
How it Works
File Structure
The pattern requires a specific file organization:
src/utils/err/
├── $.ts # Public API (points to $$.ts)
├── $$.ts # Re-exports from implementation
└── err.ts # Implementation
Implementation
1. Implementation file (err.ts
):
// Contains all the actual implementation
export const fromUnknown = (error: unknown): Error => {
// ... implementation
}
export const toString = (error: Error): string => {
// ... implementation
}
2. Internal barrel ($$.ts
):
// Re-exports everything from implementation
export * from './err.js'
3. Public API ($.ts
):
/**
* Error handling utilities.
*
* Provides functions for working with JavaScript errors.
*/
export * as Err from './$$.js'
The key is the export * as Err from './$$.js'
pattern in the public API file. This creates the drillable namespace.
Package Configuration
Your package.json
must expose both entry points:
{
"exports": {
".": "./build/index.js", // Main package entry
"./err": "./build/utils/err/$.js" // Subpath entry
}
}
Detection
The documentation generator automatically detects the Drillable Namespace Pattern by looking for:
- An entrypoint file with
export * as Name from './$'
or similar pattern - The pattern references an implementation file (often
$$.ts
) - The package exports both the main entry and the subpath
When detected, the API documentation will show both import forms:
// Namespace import from main package
import { Err } from '@wollybeard/kit'
// OR namespace import from subpath
import * as Err from '@wollybeard/kit/err'
Examples from Kit
Error Handling (Err
)
// Both forms work identically
import { Err } from '@wollybeard/kit'
import * as Err from '@wollybeard/kit/err'
// Use the API
const error = new Error('Something went wrong')
Err.toString(error)
Testing Utilities (Test
)
// Named import from main package
import { Test } from '@wollybeard/kit'
Test.describe('my test')
.i<string>()
.o<boolean>()
.cases()
/* ... */
.test()
// Namespace import from subpath
import * as Test from '@wollybeard/kit/test'
Test.describe('my test')
// ... same API
Numeric Types (Num
)
// Both forms provide the same namespaced API
import { Num } from '@wollybeard/kit'
import * as Num from '@wollybeard/kit/num'
// Access nested namespaces
Num.Positive.from(42)
Num.InRange.from(5, 0, 10)
Comparison with Simple Entrypoints
Simple Entrypoint
For modules without the drillable pattern, only the subpath import is available:
// Only form available
import { someFunction } from '@wollybeard/kit/utilities'
The API documentation shows a single import form.
Drillable Namespace
Modules using the pattern offer both:
// Form 1: From main package
import { Module } from '@wollybeard/kit'
// Form 2: From subpath
import * as Module from '@wollybeard/kit/module'
The API documentation explicitly shows both forms, helping users choose.
Best Practices
- Use meaningful names: The namespace name should clearly represent the module's purpose
- Keep cohesive: Only group related functionality under one namespace
- Document clearly: Add JSDoc to the
export * as Name
statement to describe the module - Consistent API: Ensure all functions use consistent naming and patterns
- Avoid deep nesting: Keep namespace hierarchies shallow (2-3 levels max)
Migration Guide
To convert an existing module to use the Drillable Namespace Pattern:
- Create the file structure if not already present (
$.ts
,$$.ts
, implementation) - Add the export pattern in
$.ts
:typescriptexport * as ModuleName from './$$.js'
- Update package.json to expose the subpath
- Add JSDoc to describe the module's purpose
- Regenerate documentation to see both import forms
The pattern is backward compatible - existing subpath imports continue to work.