Skip to content

Labeled-args components (no HTML-attrs base) never get a nameable props type — blocks wrapper/override pattern #155

Description

@jagguji

Problem

Whether a component binds via the type props = {...} + external make: React.component<props> form, or the classic @react.component external make: (~p: t=?, …) => React.element labeled-argument form, is decided entirely by recordProps = !!(ir.attrsBase && ir.attrsBase.ref) || baseSpreads.length > 0 (src/emit.mjs, around the "3. the external binding" comment). Record-props only happens when the component's props extend a recognized *HTMLAttributes interface, or share a base type with sibling components (#82/#131). A component with a plain, self-contained props object — no HTML-attrs extension, no shared base — always falls to labeled args, even though the two forms are behaviorally identical (@react.component on labeled args auto-derives the same props record internally; JSX call sites are unaffected either way).

Concretely, in @juspay/blend-design-system: Button's props extend ButtonHTMLAttributes<HTMLButtonElement> → gets type props = {...HtmlAttrs.buttonHTMLAttributesOmitClassNameDisabledOnClickStyle, ...} + React.component<props>. AccordionItem's props (value, title, subtext?, …) are a plain object type with no extends clause → gets the labeled-args form, and no nameable props type is ever emitted for it.

Impact

Medium — a plain type props = {...} is what lets a consumer write a thin wrapper component around a binding, e.g.:

module Button = {
  type props = JuspayRescriptBlend.Button.props
  @react.componentWithProps
  let make = (props: props) => {
    let handleClick = e => { /* analytics, then */ props.onClick->Option.forEach(f => f(e)) }
    <JuspayRescriptBlend.Button {...props} onClick=handleClick />
  }
}

This only compiles because Button.res exports a nameable props type. Components emitted via the labeled-args form (like AccordionItem) have no such type to reference, so this wrapper-and-override pattern — spread all props through, override just one (onClick, renderItem, …) — isn't possible for them today, even though nothing about their actual prop shape prevents it.

Suggested fix

Make record-props emission independent of attrsBase/baseSpreads — e.g. default to type props = {...} + React.component<props> for every component (ir.props.length > 0), keeping the labeled-args form only as an explicit opt-out (mirroring how --no-html-attrs already opts out of the HTML-attrs spread specifically). Off the top: the ~102 goldens that currently assert the labeled form for non-attrsBase components would need updating, and docs/TYPE_MAPPING.md would need a note that record-props is now unconditional (or gated behind a new default-on flag, if a transition period is wanted).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions