Type System Consistency Recommendations
This document addresses the identified inconsistencies in the ReX content system’s type definitions and proposes standardization strategies to improve code clarity, maintainability, and developer experience.
1. Content Representation Standardization
Issue: Content vs RawContent Inconsistency
The content system currently uses two different interfaces to represent content:
Content
interface in core types (withdata: string | Uint8Array
)RawContent
interface in adapter types (withdata: string
only)
This creates confusion about which interface to use and introduces type safety issues when passing content between layers.
Recommended Strategy
Adopt a layered content type system with clear transformations:
- Define
Content
as the primary interface throughout the system:
interface Content<T = string | Uint8Array> {
data: T
contentType: string
metadata?: ContentMetadata
}
- Define adapter-level content as a specialization:
type AdapterContent = Content<string>
- Create explicit transformation functions:
// For adapters that need to convert binary to string
function serializeContent(content: Content): AdapterContent {
if (typeof content.data === 'string') {
return content as AdapterContent
}
return {
...content,
data: binaryToString(content.data),
}
}
// For retrieving content from adapters
function deserializeContent(
content: AdapterContent,
originalContentType?: string
): Content {
// If the content should be binary, convert it back
if (originalContentType && isBinaryType(originalContentType)) {
return {
...content,
data: stringToBinary(content.data),
}
}
return content
}
- Document clear usage patterns:
- Core API methods accept and return the generic
Content
type - Adapters internally work with
AdapterContent
- The store layer manages the transformation between types
This approach maintains flexibility for binary data while providing type safety and explicit conversions.
2. Metadata Structure Alignment
Issue: Inconsistent Metadata Fields and Types
The metadata structure differs between core and adapter definitions:
- Core
ContentMetadata
: UsesDate | string
for timestamps, complexversion
object - Adapter
ContentMetadata
: Uses numeric timestamps, simpleversion
string
Recommended Strategy
Create a unified metadata model with clear serialization:
- Define a canonical
ContentMetadata
interface used throughout the system:
interface ContentMetadata {
// Common fields
title?: string
description?: string
// Temporal fields with ISO string representation
createdAt?: string // ISO date string
updatedAt?: string // ISO date string
// Author information with consistent structure
author?: string | { name: string; email?: string; url?: string }
// Version information with consistent structure
version?: {
id: string
number: number
hash?: string
}
// Allow additional metadata
[key: string]: any
}
- Create helper utilities for working with dates:
// Convert any date representation to ISO string
function normalizeDate(date?: Date | string | number): string | undefined {
if (!date) return undefined
if (date instanceof Date) return date.toISOString()
if (typeof date === 'number') return new Date(date).toISOString()
return date // Assume already a string
}
// Get Date object from metadata string
function getDateObject(isoString?: string): Date | undefined {
return isoString ? new Date(isoString) : undefined
}
- Implement transformer functions between serialized and domain forms:
// For internal use (with rich types)
interface DomainMetadata extends ContentMetadata {
createdAt?: Date
updatedAt?: Date
}
// Convert domain objects to storage-ready format
function serializeMetadata(metadata: DomainMetadata): ContentMetadata {
return {
...metadata,
createdAt: metadata.createdAt?.toISOString(),
updatedAt: metadata.updatedAt?.toISOString(),
}
}
// Convert storage format to domain objects
function deserializeMetadata(metadata: ContentMetadata): DomainMetadata {
return {
...metadata,
createdAt: metadata.createdAt ? new Date(metadata.createdAt) : undefined,
updatedAt: metadata.updatedAt ? new Date(metadata.updatedAt) : undefined,
}
}
This approach provides a consistent interface while allowing flexible implementation details.
3. Specialized Content Types
Issue: Specialized Content Types Lack Adapter Support
The core types define specialized content interfaces (MDXContent, ImageContent) without clear patterns for how adapters should handle them.
Recommended Strategy
Create a generic content specialization pattern:
- Define a content specialization pattern with type guards:
// Base specialization pattern
interface SpecializedContent<T extends string = string> extends Content {
contentType: T
// Additional specialized fields can go here
specialized?: Record<string, any>
}
// Type guard for checking specialized content
function isSpecializedContent<T extends string>(
content: Content,
type: T
): content is SpecializedContent<T> {
return content.contentType === type
}
// MDX specialized content
interface MDXContent extends SpecializedContent<'text/mdx'> {
specialized: {
frontmatter: Record<string, any>
compiledCode?: string
components?: string[]
}
}
// Type guard for MDX content
function isMDXContent(content: Content): content is MDXContent {
return (
isSpecializedContent(content, 'text/mdx') &&
!!content.specialized?.frontmatter
)
}
- Create adapter serialization helpers:
// Adapter-level serialization
function serializeSpecializedContent<T extends SpecializedContent>(
content: T
): AdapterContent {
return {
data:
typeof content.data === 'string'
? content.data
: binaryToString(content.data),
contentType: content.contentType,
metadata: {
...content.metadata,
// Store specialized data in metadata to preserve it
__specialized: content.specialized,
},
}
}
// Adapter-level deserialization
function deserializeSpecializedContent(content: AdapterContent): Content {
// Extract specialized data
const { __specialized, ...regularMetadata } = content.metadata || {}
// Basic content without specialization
const baseContent: Content = {
data: content.data,
contentType: content.contentType,
metadata: regularMetadata,
}
// If no specialized data, return basic content
if (!__specialized) return baseContent
// Return specialized content
return {
...baseContent,
specialized: __specialized,
}
}
This pattern provides a consistent way to handle specialized content while maintaining adapter simplicity.
4. Optional Methods and Capabilities
Issue: Unclear Handling of Optional Adapter Methods
Adapters define many optional methods (getMetadata?
, move?
, etc.) without clear documentation on how the system handles missing capabilities.
Recommended Strategy
Create a capability detection and fallback system:
- Define adapter capabilities as an enumeration:
enum AdapterCapability {
// Basic capabilities
READ = 'read',
WRITE = 'write',
DELETE = 'delete',
LIST = 'list',
// Enhanced capabilities
METADATA = 'metadata',
MOVE = 'move',
COPY = 'copy',
WATCH = 'watch',
STATS = 'stats',
// Environment-specific
DIRECTORY = 'directory',
PERSISTENT = 'persistent',
}
- Add capability detection to adapters:
interface ContentAdapter {
// Core methods...
// Capability detection
hasCapability(capability: AdapterCapability): boolean
// Get all available capabilities
getCapabilities(): AdapterCapability[]
}
- Implement fallbacks for missing capabilities:
// Base adapter implementation with fallbacks
class BaseAdapter implements ContentAdapter {
// Core required methods...
// Optional methods
async getMetadata?(uri: string): Promise<ContentMetadata> {
// Fallback: read the full content and extract metadata
const content = await this.read(uri)
return content.metadata || {}
}
async move?(source: string, destination: string): Promise<void> {
// Fallback: read, write, delete
const content = await this.read(source)
await this.write(destination, content)
await this.delete(source)
}
// Capability detection
hasCapability(capability: AdapterCapability): boolean {
switch (capability) {
case AdapterCapability.READ:
case AdapterCapability.WRITE:
case AdapterCapability.DELETE:
case AdapterCapability.LIST:
return true
case AdapterCapability.METADATA:
return typeof this.getMetadata === 'function'
case AdapterCapability.MOVE:
return typeof this.move === 'function'
// Other capabilities...
default:
return false
}
}
getCapabilities(): AdapterCapability[] {
return Object.values(AdapterCapability).filter(cap =>
this.hasCapability(cap)
)
}
}
- Document usage patterns for capability-aware code:
// Example of capability-aware code
async function moveOrCopy(
adapter: ContentAdapter,
source: string,
destination: string
): Promise<void> {
if (adapter.hasCapability(AdapterCapability.MOVE)) {
await adapter.move!(source, destination)
} else {
// Fallback to copy and delete
const content = await adapter.read(source)
await adapter.write(destination, content)
await adapter.delete(source)
}
}
This approach provides clear expectations for adapter implementations while maintaining flexibility.
5. Date Representation Standardization
Issue: Mixed Date Representations
The system uses various date representations including Date
objects, ISO strings, and numeric timestamps.
Recommended Strategy
Standardize on ISO string representations for API boundaries:
- Use ISO string representations at API boundaries and storage:
interface ContentMetadata {
createdAt?: string // ISO format: "2023-04-15T12:30:45.123Z"
updatedAt?: string // ISO format: "2023-04-15T12:30:45.123Z"
// Other fields...
}
- Provide utility functions for working with dates:
// Date utilities
const DateUtils = {
// Convert to ISO string
toISOString(date?: Date | string | number): string | undefined {
if (!date) return undefined
if (date instanceof Date) return date.toISOString()
if (typeof date === 'number') return new Date(date).toISOString()
return date // Assume already a string
},
// Parse to Date object
toDate(date?: string | number): Date | undefined {
if (!date) return undefined
return new Date(date)
},
// Format for display
format(date?: string | Date, format: string = 'short'): string {
if (!date) return ''
const dateObj = typeof date === 'string' ? new Date(date) : date
// Use Intl for proper formatting
switch (format) {
case 'short':
return new Intl.DateTimeFormat('en', {
dateStyle: 'short',
timeStyle: 'short',
}).format(dateObj)
case 'long':
return new Intl.DateTimeFormat('en', {
dateStyle: 'long',
timeStyle: 'long',
}).format(dateObj)
// Other formats...
default:
return dateObj.toISOString()
}
},
}
- Document clear date handling expectations:
- All dates are stored as ISO strings in content metadata
- ISO strings are used in all API methods
- Date objects can be used internally in application code
- Always convert to ISO strings before storing
- Use the utility functions for consistent handling
This approach provides a clear standard while maintaining flexibility for different usage patterns.
Implementation Roadmap
Phase 1: Core Type Definition Standardization
- Update the
Content
andContentMetadata
interfaces in core types - Create explicit adapter content type definitions
- Implement serialization/deserialization utilities
- Update documentation to reflect the new type system
Phase 2: Capability System Implementation
- Define the
AdapterCapability
enumeration - Add capability detection methods to the adapter interface
- Implement fallbacks for optional methods
- Update adapter implementations to support capability detection
Phase 3: Specialized Content Integration
- Define the specialized content pattern
- Create type guards for different content types
- Implement serialization helpers for specialized content
- Update documentation with usage examples
Phase 4: Documentation and Migration
- Create a terminology glossary
- Update documentation with clear type usage guidelines
- Add cross-references between related concepts
- Create visualizations of the type system
- Provide migration guides for existing code
Conclusion
By implementing these recommendations, the ReX content system will achieve greater consistency, clarity, and maintainability. The proposed strategies:
- Maintain the existing architectural strengths
- Resolve current inconsistencies in the type system
- Provide clear patterns for extension and specialization
- Improve developer experience through clearer documentation
This will result in a more robust foundation for future development while making the system more approachable for new developers.