Skip to content

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 (with data: string | Uint8Array)
  • RawContent interface in adapter types (with data: string only)

This creates confusion about which interface to use and introduces type safety issues when passing content between layers.

Adopt a layered content type system with clear transformations:

  1. Define Content as the primary interface throughout the system:
typescript
interface Content<T = string | Uint8Array> {
  data: T
  contentType: string
  metadata?: ContentMetadata
}
  1. Define adapter-level content as a specialization:
typescript
type AdapterContent = Content<string>
  1. Create explicit transformation functions:
typescript
// 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
}
  1. 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: Uses Date | string for timestamps, complex version object
  • Adapter ContentMetadata: Uses numeric timestamps, simple version string

Create a unified metadata model with clear serialization:

  1. Define a canonical ContentMetadata interface used throughout the system:
typescript
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
}
  1. Create helper utilities for working with dates:
typescript
// 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
}
  1. Implement transformer functions between serialized and domain forms:
typescript
// 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.

Create a generic content specialization pattern:

  1. Define a content specialization pattern with type guards:
typescript
// 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
  )
}
  1. Create adapter serialization helpers:
typescript
// 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.

Create a capability detection and fallback system:

  1. Define adapter capabilities as an enumeration:
typescript
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',
}
  1. Add capability detection to adapters:
typescript
interface ContentAdapter {
  // Core methods...

  // Capability detection
  hasCapability(capability: AdapterCapability): boolean

  // Get all available capabilities
  getCapabilities(): AdapterCapability[]
}
  1. Implement fallbacks for missing capabilities:
typescript
// 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)
    )
  }
}
  1. Document usage patterns for capability-aware code:
typescript
// 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.

Standardize on ISO string representations for API boundaries:

  1. Use ISO string representations at API boundaries and storage:
typescript
interface ContentMetadata {
  createdAt?: string // ISO format: "2023-04-15T12:30:45.123Z"
  updatedAt?: string // ISO format: "2023-04-15T12:30:45.123Z"
  // Other fields...
}
  1. Provide utility functions for working with dates:
typescript
// 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()
    }
  },
}
  1. 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

  1. Update the Content and ContentMetadata interfaces in core types
  2. Create explicit adapter content type definitions
  3. Implement serialization/deserialization utilities
  4. Update documentation to reflect the new type system

Phase 2: Capability System Implementation

  1. Define the AdapterCapability enumeration
  2. Add capability detection methods to the adapter interface
  3. Implement fallbacks for optional methods
  4. Update adapter implementations to support capability detection

Phase 3: Specialized Content Integration

  1. Define the specialized content pattern
  2. Create type guards for different content types
  3. Implement serialization helpers for specialized content
  4. Update documentation with usage examples

Phase 4: Documentation and Migration

  1. Create a terminology glossary
  2. Update documentation with clear type usage guidelines
  3. Add cross-references between related concepts
  4. Create visualizations of the type system
  5. 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:

  1. Maintain the existing architectural strengths
  2. Resolve current inconsistencies in the type system
  3. Provide clear patterns for extension and specialization
  4. 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.

Released under the MIT License.