PropType class#127
Conversation
Types are now PropType instances rather than method-pack registrations.
Built-ins are registered singletons; derivatives are `Object.create(parent)`,
so option lookup walks the JS prototype chain — no spec merging or copying.
- `PropType.for(input, { fallback })` is the public resolver; returns the
registered singleton, a fresh derivative for specs with extras, or the
fallback. `PropType.normalizeIs` is the single source of string→ctor
resolution.
- `PropType.prototype.equals/parse/stringify` are dispatchers that handle
null/identity short-circuits and walk the instance chain for an override
on `obj.spec`. The passed spec object is stored verbatim as `instance.spec`
— no allocation per construction.
- `ListType` / `DictionaryType` are abstract bases under `util/`, with
`static nestedSpecKeys` declaring which option keys hold nested type specs
(only those are resolved + stored as own properties on the instance;
everything else stays on `this.spec`).
- New `/plugins/types` endpoint exposes built-in singletons by name.
- Drop the auto-spacing heuristic in `joinItems`; custom separators are now
used verbatim. Default separator is `", "` so the common case is unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Abstract types (IterableType, formerly DictionaryType) used to be JS subclasses of PropType, encoding the type chain twice: once in `obj.super` (walked by the dispatcher) and once in `class X extends Y` (ignored by the dispatcher). The duplication surfaced as a wart — `parseItems` couldn't be a generator without colliding with the dispatched `parse` method, and Dictionary couldn't inherit Iterable's splitting because they were sibling classes. PropType is now the sole class. Abstracts are PropType instances registered by `name`; concrete types declare `extends: <abstract>` to decouple the chain parent from `is`. The dispatcher walks one chain for everything. - IterableType.parseItems (raw splitter) + parse (typed, generator) + stringify + equals — all streaming, single-pass. - MapType absorbs the former DictionaryType (Map IS the dictionary in JS; Dictionary was a fabricated abstract). ObjectType extends MapType. - Concrete types (array.js, set.js, object.js, map.js) live in one-file-per- type form; each consumes its parent's iterator with the appropriate container constructor. - `subTypes` spec key replaces `nestedSpecKeys`; unspecified sub-types default to PropType.any (set at the abstract root, inherited via the prototype chain) so `this.values.parse(v)` works unconditionally. - split() extracted to util/split.js. - `isA(other)` helper replaces `instanceof` for abstract-type checks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refresh the props README's Custom types walkthrough for the new single-class architecture and add a dedicated types/README covering the spec-key catalog, abstract helper methods (parseItems, parseEntries), public API, and a parametrized custom-type example (Length). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `Type` suffix was a class-era artifact; now that abstracts are PropType instances, "Iterable" is the natural name and matches the user-facing `extends: Iterable` keyword. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Because Claude f*ed up.
PropType used to walk a custom \`obj.super\` chain for every method
dispatch (\`equals\` / \`parse\` / \`stringify\` plus auto-wrapped helpers).
That was redundant given \`Object.create(parent)\` already wires up the JS
prototype chain — the for-loop was a workaround for spec being nested.
Now \`init()\` lifts every spec property onto the instance directly: keys
that collide with prototype names (\`equals\`, \`parse\`, \`stringify\`, etc.)
get a \`spec_\` prefix; everything else (\`parseItems\`, \`parseEntries\`,
sub-types, separators…) goes straight on. Derivatives inherit via JS
prototype chain; the for-loop and \`dispatch\` helper are gone.
\`from(spec)\` on the prototype lets any PropType spawn derivatives
directly (\`Iterable.from({values: Number})\`). The constructor delegates
to it.
The type files no longer need \`X.spec.parse.call(this, value)\` reach-in:
- Iterable splits \`parse\` into a generator \`parsedItems(value)\` + a
terminal \`parse(value) = [...this.parsedItems(value)]\`.
- ArrayType is now just \`{is: Array, extends: Iterable}\` — inherits
everything.
- SetType.parse = \`new Set(this.parsedItems(value))\`.
- MapType similarly adds \`parsedEntries(value)\` + terminal parse.
- ObjectType.parse uses \`this.parsedEntries(value)\`; stringify inlined
(5 lines duplicated from MapType in exchange for no spec reach-in).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
These intermediate generators are workarounds for not having an ergonomic way to express "lazy typed iteration with `this` binding" inline. `Iterator.prototype.map` solves this, but it's still Baseline 2025 — too fresh to rely on yet. When it's widely available, both methods can be inlined into their respective `parse` and removed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Still needs work but oh well
|
I left a few comments and proposals and submitted a PR with a few more fixes. I also tested it with the color elements. There was only one change needed to adopt the changes introduced by this PR: to register the |
There was a problem hiding this comment.
This file is dead code now. Should we just remove it?
|
A few spots where the docs describe the mechanism slightly differently from what the code does. The user-facing behavior described is correct in each case — it's the "how" that's off. 1. "Auto-wrapped … super-walking dispatcher" (spec-keys table)
In Possible fix: "Stored as own properties at construction and inherited by derivatives via the prototype chain, so 2. Architecture paragraph — "The dispatcher walks
|
Could you elaborate on this? It should not need registration, the |
Co-Authored-By: Dmitry Sharabin <dmitrysharabin@gmail.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ctly Adds an `Unknown` stand-in class (Color.js-shaped: string constructor, toString, equals) and a test group exercising the README's claim that `type: SomeClass` Just Works without `PropType.register()` — parse builds a `new SomeClass(value)`, stringify uses toString, equals uses `a.equals(b)`. Also flips "Unregistered constructor yields the default fallback" to "…yields a derivative carrying its `is`" to encode the contract, and adds an "Unresolvable string still yields the default fallback" test to keep the typo/missing-import carve-out pinned down.
…ctors Co-Authored-By: Claude <claude@users.noreply.github.com>
The issue is with Several color-elements props declare
That said, this could also be fixed upstream — This bug is the last blocker. |
The dispatcher's spec_X indirection is replaced by per-method get_X
transforms invoked at lift time. Each user spec method is wrapped with
the null/identity short-circuit at init, then stored under its natural
name — equals/parse/stringify on the prototype are bootstrapped the
same way via get_X() with no spec.
this.super lazy-instantiates a Proxy over Object.create(parent) that
binds function reads to self, giving JS-class-style super semantics
without recursion: super.parse resolves directly to parent's wrapped
function rather than the dispatcher.
Chain pointer renamed from super (data) to parent. isA rewritten as
this === other || isPrototypeOf — derivatives are Object.create(parent),
so the JS prototype chain is the type chain. PropType.for() collapses
bare {is: X} to X upstream so both input forms hit the cached-lookup
path via register(); constructor stays pure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The issue with unregistered constructors is fixed. Verified with the color elements: No need to register |
Docs-vs-code reviewFlagged by Claude and Codex. P2 —
|
- types/README: recommend this.super.method() (the workaround was backwards) - types/README: Length example uses this.unit, not this.spec.unit - types/README: "spread into array" instead of Array.from to match array.js - PropType.js: drop stale spec_ prefix JSDoc, describe get_<name> transforms and the super proxy - props/README: type-options table now lists separator/joiner/pairs and applies keys to dictionaries (Object extends MapType) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The block comments had grown long enough to interfere with reading the code. Trimmed without dropping anything load-bearing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DmitrySharabin
left a comment
There was a problem hiding this comment.
I think we fixed all that we spotted during the reviews. If not, we'll fix it in the follow-up work. LGTM!
Summary
Rewrite of the prop-type system. Types are now first-class
PropTypeinstances composed via JS prototype-chain inheritance, replacing the previous design where types were entries in an opaque map of{equals, parse, stringify}method packs keyed on JS constructor.The motivating problem: with method-pack registrations, there was no Type instance, no inheritance, no shared behavior between Array/Set or Object/Map without per-method duplication. Derivative configurations like
{is: Array, values: Number}were re-resolved through ad-hoc spec merging on every lookup.Architecture
One class.
PropTypeis the sole class. Type instances hold their spec verbatim and useObject.create(parent)for derivatives — every option lookup walks the JS prototype chain, no merging, no copying.Registry, single map. Keyed on JS constructor (
is) for concrete types and on stringnamefor abstract types.PropType.for(input, {fallback})resolves any user-facing identifier (PropType instance, constructor, string, spec object) to the right instance: registered singleton, fresh derivative for specs with extras, or fallback.Abstract vs concrete. Abstracts (currently just
IterableType) are registered withnameand nois— they exist to be inherited from, not to claim a JS constructor. Concrete types useextends: <abstract>to decouple their chain parent fromis, e.g.{is: Array, extends: IterableType}.One dispatch chain.
PropType.prototype.equals/parse/stringifywalkobj.super, dispatching to the first override found onobj.spec. The same walker handles auto-wrapped helper methods (e.g.parseItems,parseEntries), so abstracts can publish arbitrary helpers and descendants invoke them as plainthis.x(...).Streaming pipeline.
IterableType.parseItems(raw splitter) andIterableType.parse(typed) are both generators.MapTypeextendsIterableType, reusing inheritedthis.parseItems(value)for raw string parts and splitting each on:in its ownparseEntriesto yield[key, value]tuples. Concrete types (array.js,set.js,map.js,object.js) consume the iterator with the appropriate container constructor (Array.from,new Set,new Map,Object.fromEntries). Each input value flows through the chain exactly once, no intermediate arrays.Map IS the dictionary. There's no separate
DictionaryTypeabstract —Mapis the canonical JS dictionary,Objecta constrained realization.ObjectType extends MapType, inheriting the entry-parsing pipeline.Spec keys
isextendsregistry.get(is)(e.g. concretes extending an abstract). Accepts aPropTypeinstance or a name string.nameis.subTypes["values"]for Iterable,["keys", "values"]for Map). Resolved toPropTypeinstances at construction; unspecified keys default toPropType.any(inherited via the prototype chain), sothis.values.parse(v)works unconditionally.equals/parse/stringifythis.x(...).Public API additions
PropType.for(input, { fallback })— universal type resolver.PropType.register(spec)— register a built-in or custom type.instance.isA(otherType)— chain membership check (replaces theinstanceofchecks that don't apply when abstracts aren't JS classes).import { PropType, Iterable } from "nude-element/props"for extension. Other built-in singletons can be reached viaPropType.for(name).Symbol.toStringTagonPropTypesoconsole.log(arrayType)showsArrayType.File layout (under
src/plugins/props/)util/PropType.js— the class.util/split.js— shared string splitter (pair-aware, fault-tolerant).types/iterable.js— abstract.types/array.js,set.js,map.js,object.js— one file per concrete iterable/dictionary type.types/basic.js—Boolean,Number,Function.Behavior changes worth flagging
joinItemsis gone — custom separators are used verbatim. Default separator stays", "so the common case is unchanged.Test plan
npm test— 163/167 pass (4 pre-existing skips, 0 failures).test/PropType.jscoversfor()/register()/ derivation / dispatch / built-in primitives / list & dict behavior / nested composition /isA.extends: IterableType.keys/valuesstill round-trip cleanly.🤖 Generated with Claude Code