You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Reactive props need three pieces today: static properties = { count: { type: Number } }, a declare count: number, and a default. The declare is pure boilerplate that exists only because TypeScript cannot type a class's instance members from a value in that class's own static field. The feasibility research in #531 proved a declare-free DX IS achievable while holding every webjs invariant (no build, source-is-runtime, erasable-TS, no decorators).
Design / approach
Ship a typed base-class factory. The user writes the prop shape once, inside a Component({ ... }) call the class extends, and the types flow into this:
classCounterextendsComponent({count: prop(Number,{reflect: true,default: 0}),student: prop(Student),tags: prop<Tag[]>(Array,{default: ()=>[]}),// explicit type for richer shapesmode: prop<'light'|'dark'>(String,{default: 'light'}),}){render(){returnhtml`${this.count}${this.student.name}${this.mode}`;}// all typed, NO declare}Counter.register('counter-x');
prop(Number) infers number, prop(Student) infers Student; a richer type than the constructor implies is given once as prop<T>(Ctor).
Component(shape) returns class extends WebComponent { static properties = shape }; a mapped type { [K in keyof S]: TypeOf<S[K]> } intersected with the base gives this its typed members.
All three were verified in Research a simpler reactive-prop DX than the static-properties + declare pattern #531: tsc strict + erasableSyntaxOnly PASS, module.stripTypeScriptTypes leaves valid runtime (type args erase in place, the factory call ships), and an end-to-end run against the real WebComponent works with no change to _initializeProperties.
Design decision to settle first (do not skip): REPLACE the static properties + declare model with the factory, or ship the factory ALONGSIDE it. Replacing is a single clean model and is viable because webjs has no users yet (no backward-compat burden), but it is a full migration of tooling/docs/apps. Coexisting is safer but doubles the prop-declaration API surface. Confirm the direction with the owner before building.
New exports in packages/core/index.js (and the browser bundle): prop() and Component(). Runtime is thin: Component(shape) returns an anonymous WebComponent subclass with static properties = normalize(shape); prop(type?, opts?) returns a PropertyDeclaration (handle the prop(opts) no-constructor overload at runtime by sniffing whether arg1 is callable).
Types: add the Infer<C> constructor-to-type map, the prop overloads, and the Component factory signature to packages/core/src/component.d.ts (next to PropertyDeclaration and the WebComponent class type). The default option already exists there (PR feat: declarative default option for reactive props #575).
The runtime accessor in packages/core/src/component.js_initializeProperties (~L548-589) needs NO change; it already reads Ctor.properties. The default handling landed in PR feat: declarative default option for reactive props #575.
webjs check: the reactive-props-use-declare rule (packages/server/src/check.js ~L77 and ~L443-462) only scans static properties. In the factory model there is no static-field initializer to clobber, so decide whether the rule still applies (it should still fire for the OLD model if coexisting). Add a rule/lint for misuse of the factory if warranted.
Broad surfaces that assume the static-field model and must be synced: @webjsdev/intellisense (packages/editors/intellisense/src, re-vendor for nvim after edits), MCP list_components (packages/mcp/src), scaffold templates (packages/cli/templates/), docs site (docs/app/docs/components/page.ts), AGENTS.md, agent-docs/components.md, agent-docs/lit-muscle-memory-gotchas.md, and the four dogfood apps if migrated.
A prop() used as a class FIELD is clobbered by class-field semantics; it MUST be a factory argument, not an instance field.
Keep prop(Object) honest: a plain Object infers a loose type, so rich object shapes need the explicit prop<Shape>(Object) phantom.
Invariants to respect
AGENTS.md invariant 1 (server-only boundary), 10 (erasable TS, no decorators), and the no-build / source-is-runtime promise. The factory call must ship and run as written.
Acceptance criteria
Design decision (replace vs coexist) recorded on this issue before implementation.
Component() + prop() exported and working: a component declares props with NO declare line and this.<prop> is fully typed.
tsc strict + erasableSyntaxOnly passes on a real example; a wrong-type assignment is a type error (counterfactual via @ts-expect-error).
Source strips to valid runtime (assert against module.stripTypeScriptTypes).
Reactivity, reflect, default (literal + function), and state all work at runtime and SSR (unit + SSR + browser layers).
All tooling/docs surfaces above synced (or "N/A because" in the PR).
Problem
Reactive props need three pieces today:
static properties = { count: { type: Number } }, adeclare count: number, and a default. Thedeclareis pure boilerplate that exists only because TypeScript cannot type a class's instance members from a value in that class's own static field. The feasibility research in #531 proved a declare-free DX IS achievable while holding every webjs invariant (no build, source-is-runtime, erasable-TS, no decorators).Design / approach
Ship a typed base-class factory. The user writes the prop shape once, inside a
Component({ ... })call the class extends, and the types flow intothis:prop(Number)infersnumber,prop(Student)infersStudent; a richer type than the constructor implies is given once asprop<T>(Ctor).Component(shape)returnsclass extends WebComponent { static properties = shape }; a mapped type{ [K in keyof S]: TypeOf<S[K]> }intersected with the base givesthisits typed members.strict+erasableSyntaxOnlyPASS,module.stripTypeScriptTypesleaves valid runtime (type args erase in place, the factory call ships), and an end-to-end run against the realWebComponentworks with no change to_initializeProperties.Design decision to settle first (do not skip): REPLACE the
static properties+declaremodel with the factory, or ship the factory ALONGSIDE it. Replacing is a single clean model and is viable because webjs has no users yet (no backward-compat burden), but it is a full migration of tooling/docs/apps. Coexisting is safer but doubles the prop-declaration API surface. Confirm the direction with the owner before building.Implementation notes (for the implementing agent)
packages/core/index.js(and the browser bundle):prop()andComponent(). Runtime is thin:Component(shape)returns an anonymousWebComponentsubclass withstatic properties = normalize(shape);prop(type?, opts?)returns aPropertyDeclaration(handle theprop(opts)no-constructor overload at runtime by sniffing whether arg1 is callable).Infer<C>constructor-to-type map, thepropoverloads, and theComponentfactory signature topackages/core/src/component.d.ts(next toPropertyDeclarationand theWebComponentclass type). Thedefaultoption already exists there (PR feat: declarativedefaultoption for reactive props #575).packages/core/src/component.js_initializeProperties(~L548-589) needs NO change; it already readsCtor.properties. Thedefaulthandling landed in PR feat: declarativedefaultoption for reactive props #575.webjs check: thereactive-props-use-declarerule (packages/server/src/check.js~L77 and ~L443-462) only scansstatic properties. In the factory model there is no static-field initializer to clobber, so decide whether the rule still applies (it should still fire for the OLD model if coexisting). Add a rule/lint for misuse of the factory if warranted.@webjsdev/intellisense(packages/editors/intellisense/src, re-vendor for nvim after edits), MCPlist_components(packages/mcp/src), scaffold templates (packages/cli/templates/), docs site (docs/app/docs/components/page.ts),AGENTS.md,agent-docs/components.md,agent-docs/lit-muscle-memory-gotchas.md, and the four dogfood apps if migrated.Landmines
accessorkeyword are both out (verified in Research a simpler reactive-prop DX than the static-properties + declare pattern #531:accessoris a runtime syntax error on V8 and is not erasable). Do not reach for them.prop()used as a class FIELD is clobbered by class-field semantics; it MUST be a factory argument, not an instance field.prop(Object)honest: a plainObjectinfers a loose type, so rich object shapes need the explicitprop<Shape>(Object)phantom.Invariants to respect
Acceptance criteria
Component()+prop()exported and working: a component declares props with NOdeclareline andthis.<prop>is fully typed.strict+erasableSyntaxOnlypasses on a real example; a wrong-type assignment is a type error (counterfactual via@ts-expect-error).module.stripTypeScriptTypes).default(literal + function), andstateall work at runtime and SSR (unit + SSR + browser layers).