Skip to content

refactor: de-duplicate static members in WebComponentConstructor type #602

@vivek7405

Description

@vivek7405

Problem

The factory type surface in packages/core/src/component.d.ts declares the same set of static members twice: once on the WebComponentConstructor interface, and again, identically, on the factory-call return type. The duplicated block is:

shadow: boolean;
hydrate: 'visible' | undefined;
properties: Record<string, PropertyDeclaration>;
styles: CSSResult | CSSResult[] | null;
lazy?: boolean;
register(tag: string): void;
readonly observedAttributes: string[];

Because the list is hand-maintained in two places, adding or changing any static on WebComponent requires editing both copies or the constructor type and the factory-return type silently drift. This is a maintenance hazard, not a current bug (the two copies are in sync today).

Design / approach

Declare the static surface ONCE and reference it from both positions. Two viable shapes:

  1. Extract a WebComponentStatics interface and intersect it into both the WebComponentConstructor interface and the factory-call return type.
  2. Derive the statics from the abstract base via extends Omit<typeof WebComponentBase, 'new'> (the approach an earlier uncommitted WIP draft from the feat: declare-free reactive-prop DX via dual-role WebComponent #597 / feat: enforce declare-free factory DX (drop static properties + declare) #599 declare-free-factory work used). This requires WebComponentBase to be exported and its statics to be declared on the class.

Prefer whichever keeps the existing InferProps<S> instance typing and the call-signature overload intact. Option 1 is the smaller, lower-risk change; option 2 removes the most duplication but couples the constructor type to the base-class static declarations. Implementer's choice, validated by the type tests below.

Implementation notes (for the implementing agent)

  • Where to edit: packages/core/src/component.d.ts, the WebComponentConstructor interface (the static block on the construct signature) and the factory-call return type (the <S extends Record<string, any>>(shape: S): { ... } overload's static block). They are adjacent in the file.
  • Landmine: this is the type-only .d.ts overlay. There is no runtime change and must not be one. Do not touch component.js.
  • Landmine: the factory return must keep carrying the statics so MyComp.register('x-tag'), MyComp.shadow, etc. typecheck on a class X extends WebComponent({...}). Losing them is the regression to guard against.
  • Landmine: keep InferProps<S> on the instance type (new (): WebComponentBase & InferProps<S>) so factory-declared props stay typed on instances.
  • Invariant: packages/ is buildless plain JS + JSDoc (root AGENTS.md, framework-repo section). .d.ts overlays are fine and erased at runtime; do not introduce a .ts source file.
  • Invariant: index.d.ts must still declare every runtime named export (packages/core/AGENTS.md, dts-export-coverage). If you newly export WebComponentBase, ensure the export-coverage test (test/types/dts-export-coverage.test.mjs) stays green.
  • Tests: the type-level tests live in test/types/component-types.test-d.ts (factory usage: WebComponent({...}), prop<T>(), .register(...), InferProps assertions). These are the regression net. Run webjs typecheck.

Acceptance criteria

  • The static member list (shadow, hydrate, properties, styles, lazy, register, observedAttributes) is declared in exactly one place and referenced from both the constructor type and the factory-call return type
  • class X extends WebComponent({...}) still typechecks X.register('x-tag'), X.shadow, and instance prop access (InferProps)
  • test/types/component-types.test-d.ts passes unchanged (or with only additive assertions)
  • Counterfactual: removing a static from the shared declaration makes the type tests fail (proves both positions actually consume the shared source)
  • webjs typecheck is green; no runtime change to component.js
  • No public-surface change, so no docs update expected

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type
No fields configured for issues without a type.

Projects

Status
In progress

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions