Skip to content

Explore a type-only layout registry for inferred layout-chain contracts #313

Description

@bcomnes

Context

Follow-up to #310 and #311 (explicit nested layouts), with the per-renderer data boundary from #294. This is a deferred design exploration, not a prerequisite or landing blocker for either PR.

The runtime layout registry is heterogeneous: one layout can accept a string and return an object that its parent consumes. Individual renderers retain explicit input/output generics, but a string-keyed runtime registry cannot currently connect those contracts across the selected chain. Page authors also have to describe types that could potentially be derived from their selected layout.

Proposal

Explore an optional, type-only layout registry that each layout extends through TypeScript module augmentation. DOMStack would export an augmentable LayoutRegistry interface from its public type entry point. Each entry would connect the discovered layout name to its actual parent, vars export, and render function types.

For example, alongside the existing exports in article.layout.ts:

// Proposed API; these registry types do not exist yet.
declare module '@domstack/static/types.js' {
  interface LayoutRegistry {
    article: {
      parentLayout: typeof parentLayout
      vars: typeof vars
      render: typeof articleLayout
    }
  }
}

The root layout would register similarly, without a parent. Reference the actual exports with typeof instead of independently repeating the parent name, vars shape, and render signature. This would be a compile-time registry, not a runtime global object or a replacement for filesystem discovery.

With those registrations, provisional consumer helpers could look like:

import type {
  LayoutVars,
  PageForLayout,
} from '@domstack/static/types.js'

type ArticleVars = LayoutVars<'article'>
type ArticlePage = PageForLayout<'article'>

The names and exact generic parameters are illustrative. The desired outcome is that choosing a layout supplies useful inferred contracts without requiring consumers to reconstruct the chain.

Type composition rules

  • Follow the literal parentLayout declarations recursively.
  • Derive layout vars contributions in outer-to-inner order, then account for global and page/frontmatter contributions where their types are known.
  • Model override precedence rather than blindly intersecting conflicting properties into never.
  • Distinguish supplied defaults from the vars a renderer requires, and reject incompatible overrides where statically knowable.
  • Infer the page content type expected by the innermost layout.
  • Check each child layout's awaited render result against its parent's accepted children type.
  • Derive the outermost renderer's result separately from DOMStack's final serialized HTML string.

Vars merge, but render types compose. For example, if article accepts a string and returns a Frame, and root accepts a Frame, the page must return a string. The content type is not an intersection of string and Frame.

Preserve the existing data boundary

The registry must not expose an ancestor's subscribed data to its children or to the page. Each renderer keeps its own declared data contract. The union of dataDeps across the page and layout chain remains an output-invalidation concern, not a merged renderer data object. Any registry support for data contracts should derive from the existing renderer declarations rather than creating another independent source of truth.

Constraints and questions to resolve

  • Keep the current explicit generic APIs available for unregistered layouts and JavaScript consumers.
  • Registration files must be included in the site's TypeScript program; type-only declarations must not create runtime imports or watch dependencies.
  • Determine how JavaScript/JSDoc projects can opt in, potentially through a companion declaration file.
  • Prevent circular inference when a renderer registers its own type; do not require its signature to depend recursively on that same registry entry.
  • Decide how literal versus dynamically selected layout names affect precision and fallbacks.
  • Retain runtime checks for actual exports, missing parents, and cycles; TypeScript does not validate Markdown/HTML frontmatter or arbitrary dynamic modules automatically.
  • Consider registry name collisions when several sites share a TypeScript program.
  • Keep recursive-type diagnostics and compiler performance practical.
  • Evaluate generated registrations only if manual registration proves burdensome; code generation is not assumed by this proposal.

Validation for a future implementation

Add positive and negative compile-time tests covering mixed render values, asynchronous layouts, inherited vars, compatible and incompatible overrides, missing names, cycles, and page/layout boundary mismatches. Verify that each renderer's data remains isolated and that sites using the existing APIs continue to type-check. Include clear examples showing local registration and simplified final page usage.

Sequencing

Leave #311 and #294 focused on their current runtime features and fixes. Prototype this separately after those changes land, then decide whether the typing benefits justify the public API and compiler complexity.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions