@lume/element
Version:
Create Custom Elements with reactivity and automatic re-rendering.
714 lines (591 loc) • 28.4 kB
text/typescript
import {render} from 'solid-js/web'
import {createRoot, getOwner, runWithOwner, type Owner} from 'solid-js'
// isPropSetAtLeastOnce__ was exposed by classy-solid specifically for
// @lume/element to use. It tells us if a signal property has been set at
// least once, and if so allows us to skip overwriting it with a custom
// element preupgrade value.
import {Effects, isPropSetAtLeastOnce__, startEffects, stopEffects} from 'classy-solid'
import type {
AttributeHandler,
AttributePropSpecs,
attributesToProps__,
hasAttributeChangedCallback__,
} from './decorators/attribute.js'
import type {DashCasedProps} from './utils.js'
// TODO `templateMode: 'append' | 'replace'`, which allows a subclass to specify
// if template content replaces the content of `root`, or is appended to `root`.
const HTMLElement =
globalThis.HTMLElement ??
class HTMLElement {
constructor() {
throw new Error(
"@lume/element needs a DOM to operate with! If this code is running during server-side rendering, it means your app is trying to instantiate elements when it shouldn't be, and should be refactored to avoid doing that when no DOM is present.",
)
}
}
const root = Symbol('root')
// TODO Make LumeElement `abstract`
class LumeElement extends HTMLElement {
/**
* The default tag name of the elements this class instantiates. When using
* the `@element` decorator, if this field has not been specified, it will
* be set to the value defined by the decorator.
*/
static elementName: string = ''
/**
* When using the @element decorator, the element will be automatically
* defined in the CustomElementRegistry if this is true, otherwise manual
* registration will be needed if false. If autoDefine is passed into the
* decorator, this field will be overriden by that value.
*/
static autoDefine: boolean = true
/**
* Define this class for the given element `name`, or using its default name
* (`elementName`) if no `name` given. Defaults to using the global
* `customElements` registry unless another registry is provided (for
* example a ShadowRoot-scoped registry).
*
* If a `name` is given, then the class will be extended with an empty
* subclass so that a new class is used for each new name, because otherwise
* a CustomElementRegistry does not allow the same exact class to be used
* more than once regardless of the name.
*
* @returns Returns the defined element class, which is only going to be a
* different subclass of the class this is called on if passing in a custom
* `name`, otherwise returns the same class this is called on.
*/
static defineElement(): typeof LumeElement
static defineElement(registry: CustomElementRegistry): typeof LumeElement
static defineElement(name: string): typeof LumeElement
static defineElement(name: string, registry: CustomElementRegistry): typeof LumeElement
static defineElement(
nameOrRegistry: string | CustomElementRegistry = this.elementName,
registry: CustomElementRegistry = customElements,
) {
const name = typeof nameOrRegistry === 'string' ? nameOrRegistry : this.elementName
registry = typeof nameOrRegistry === 'string' ? customElements : nameOrRegistry
if (this === LumeElement) throw new TypeError('defineElement() can only be called on a subclass of LumeElement.')
if (!name) throw new TypeError(`defineElement(): Element name cannot be empty.`)
if (registry.get(name)) throw new TypeError(`defineElement(): registry has an existing definition for "${name}".`)
// Allow the same element to be defined with multiple names.
const alreadyUsed = !!registry.getName(this)
const Class = alreadyUsed ? class extends this {} : this
Class.elementName = name
registry.define(name, Class)
return Class
}
/**
* Non-decorator users can use this to specify a list of attributes, and the
* attributes will automatically be mapped to reactive properties. All
* attributes in the list will be treated with the equivalent of the
* `@attribute` decorator.
*
* The ability to provide a map of attribute names to attribute handlers
* (`Record<string, AttributeHandler>`) has been deprecaated, and instead
* that map should be provided via the `static observedAttributeHandlers`
* property, while this property is now typed to accept only a string array
* as per DOM spec.
*/
static observedAttributes?: string[]
/**
* Non-decorator users can use this instead of `observedAttributes` to
* specify a map of attribute names to attribute handlers. The named
* attributes will automatically be mapped to reactive properties, and each
* attribute will be treated with the corresponding attribute handler.
*
* Example:
*
* ```js
* element('my-el')(
* class MyEl extends LumeElement {
* static observedAttributeHandlers = {
* foo: attribute.string(),
* bar: attribute.number(),
* baz: attribute.boolean(),
* }
*
* // The initial values defined here will be the values that these
* // properties revert to when the respective attributes are removed.
* foo = 'hello'
* bar = 123
* baz = false
* }
* )
* ```
*/
static observedAttributeHandlers?: AttributeHandlerMap
/**
* When `true`, effects created via the classy-solid `@effect` decorator
* will automatically start upon instance construction.
*
* Defaults to `false` with effects starting in `connectedCallback()`.
*/
static autoStartEffects = false;
/** Note, this is internal and used by the @attribute decorator, see attribute.ts. */
declare [attributesToProps__]?: AttributePropSpecs;
/** Note, this is internal and used by the @attribute decorator, see attribute.ts. */
declare [hasAttributeChangedCallback__]?: true
/**
* This can be used by a subclass, or other frameworks handling elements, to
* detect property values that exist from before custom element upgrade.
*
* When this base class runs (before any subclass constructor does), it will
* track any discovered pre-upgrade property values here, then subclasses
* can subequently handle (if needed, as this base class will automatically
* convert pre-upgrade properties into reactive/signal descriptors if they
* were defined to be reactive with `classy-solid`'s `@signal` decorator,
* LumeElement's `@attribute` decorator (and derivatives), or `static
* observedAttributes`.
*/
declare protected _preUpgradeValues: Map<PropertyKey, unknown>
#handleInitialPropertyValuesIfAny() {
// We need to delete initial value-descriptor properties (if they exist)
// and store the initial values in the storage for our @signal property
// accessors.
//
// If we don't do this, then DOM APIs like cloneNode will create our
// node without first upgrading it, and then if someone sets a property
// (while our reactive accessors are not yet present in the class
// prototype) it means those values will be set as value descriptor
// properties on the instance instead of interacting with our accessors
// (i.e. the new properties will override our accessors that the
// instance will gain on its prototype chain once the upgrade process
// places our class prototype in the instance's prototype chain).
//
// This can also happen if we set properties on an element that isn't
// upgraded into a custom element yet, and thus will not yet have our
// accessors.
//
// Assumption: any enumerable own props must've been set on the
// element before it was upgraded. Builtin DOM properties are
// not enumerable.
const preUpgradeKeys = Object.keys(this) as (keyof this)[]
this._preUpgradeValues = new Map()
for (const propName of preUpgradeKeys) {
const descriptor = Object.getOwnPropertyDescriptor(this, propName)!
// Handle only value descriptors.
if ('value' in descriptor) {
// Delete the pre-upgrade value descriptor (1/2)...
delete this[propName]
// The @element decorator reads this, and the class finisher
// will set pre-upgrade values.
this._preUpgradeValues.set(propName, descriptor.value)
// NOTE, for classes not decorated with @element, deferring
// allows preexisting preupgrade values to be handled *after*
// class fields have been set during Custom Element upgrade
// construction (otherwise those class fields would override the
// preupgrade values we're trying to assign here).
// TODO Once decorators are out everywhere, deprecate
// non-decorator usage, and eventually remove code intended for
// non-decorator usage such as this.
queueMicrotask(() => {
const propSetAtLeastOnce = isPropSetAtLeastOnce__(this, propName as string | symbol)
// ... (2/2) and re-assign the value so that it goes through
// a @signal accessor that got defined, or through an
// inherited accessor that the preupgrade value shadowed.
//
// If the property has been set between the time LumeElement
// constructor ran and the deferred microtask, then we don't
// overwrite the property's value with the pre-upgrade value
// because it has already been intentionally set to a
// desired value post-construction.
// (NOTE: Avoid setting properties in constructors because
// that will set the signals at least once. Instead,
// override with a new @attribute or @signal class field.)
//
// AND we handle inherited props or signal props only
// (because that means there may be an accessor that needs
// the value to be passed in). The @element decorator otherwise
// handles non-inherited props before construction
// finishes. {{
if (propSetAtLeastOnce) return
const inheritsProperty = propName in (this as any).__proto__
if (inheritsProperty) this[propName] = descriptor.value
// }}
})
} else {
// We assume a getter/setter descriptor is intentional and meant
// to override or extend our getter/setter so we leave those
// alone. The user is responsible for ensuring they either
// override, or extend, our accessor with theirs.
}
}
}
// This property MUST be defined before any other non-static non-declared
// class properties so that the initializer runs before any other properties
// are defined, in order to detect and handle instance properties that
// already pre-exist from custom element pre-upgrade time.
// TODO Should we handle initial attributes too?
// @ts-expect-error unused
#___init___ = this.#handleInitialPropertyValuesIfAny()
/**
* If a subclass provides this, it should return DOM. It is called with
* Solid.js `render()`, so it can also contain Solid.js reactivity (signals
* and effects) and templating (DOM-returning reactive JSX or html template
* literals).
*/
declare protected template?: Template
/**
* If provided, this style gets created once per ShadowRoot of each element
* instantiated from this class. The expression can access `this` for string
* interpolation.
*/
declare protected css?: string | (() => string)
/**
* If provided, this style gets created a single time for all elements
* instantiated from this class, instead of once per element. If you do not
* need to interpolate values into the string using `this`, then use this
* static property for more performance compared to the instance property.
*/
declare protected static css?: string | (() => string)
/**
* When `true`, the custom element will have a `ShadowRoot`. Set to `false`
* to not use a `ShadowRoot`. When `false`, styles will not be scoped via
* the built-in `ShadowRoot` scoping mechanism, but by a much more simple
* shared style sheet placed at the nearest root node, with `:host`
* selectors converted to tag names.
*/
readonly hasShadow: boolean = true
/** Options used for the ShadowRoot, passed to `attachShadow()`. */
shadowOptions?: ShadowRootInit;
[root]: Node | null = null
/**
* Subclasses can override this to provide an alternate Node to render into
* (f.e. a subclass can `return this` to render into itself instead of
* making a root) regardless of the value of `hasShadow`.
*/
protected get templateRoot(): Node {
if (!this.hasShadow) return this
if (this[root]) return this[root]
if (this.shadowRoot) return (this[root] = this.shadowRoot)
// TODO use `this.attachInternals()` (ElementInternals API) to get the root instead.
return (this[root] = this.attachShadow({mode: 'open', ...this.shadowOptions}))
}
protected set templateRoot(v: Node) {
if (!this.hasShadow) throw new Error('Can not set root, element.hasShadow is false.')
// @prod-prune
if (this[root] || this.shadowRoot) throw new Error('Element root can only be set once if there is no ShadowRoot.')
this[root] = v
}
/** @deprecated `root` is renamed to `templateRoot`, and `root` will be removed in a future breaking version. */
get root() {
return this.templateRoot
}
set root(val) {
this.templateRoot = val
}
/**
* Define which `Node` to append style sheets to when `hasShadow` is `true`.
* Defaults to the `this.root`, which in turn defaults to the element's
* `ShadowRoot`. When `hasShadow` is `true`, an alternate `styleRoot` is
* sometimes needed for styles to be appended elsewhere than the root. For
* example, return some other `Node` within the root to append styles to.
* This is ignored if `hasShadow` is `false`.
*
* This can be useful for fixing issues where the default append of a style
* sheet into the `ShadowRoot` conflicts with how DOM is created in
* `template` (f.e. if the user's DOM creation in `template` clears the
* `ShadowRoot` content, or etc, then we want to place the stylesheet
* somewhere else).
*/
protected get styleRoot(): Node {
return this.templateRoot
}
override attachShadow(options: ShadowRootInit) {
if (this[root]) console.warn('Element already has a root defined.')
return (this[root] = super.attachShadow(options))
}
#effects = new Effects()
#disposeRoot?: () => void
#reactiveRoot_?: Owner
get #reactiveRoot() {
if (this.#reactiveRoot_) return this.#reactiveRoot_
createRoot(dispose => {
this.#reactiveRoot_ = getOwner()!
this.#disposeRoot = () => {
dispose()
this.#reactiveRoot_ = undefined
this.#disposeRoot = undefined
}
})
return this.#reactiveRoot_!
}
// For old-style (non-decorator) effects (f.e. subclasses creating effects
// in connectedCallback).
createEffect(fn: () => void) {
runWithOwner(this.#reactiveRoot, () => this.#effects.createEffect(fn))
}
#disposeTemplate?: () => void
connectedCallback() {
const template = this.template
if (template)
this.#disposeTemplate = render(
typeof template === 'function' ? template.bind(this) : () => template,
this.templateRoot,
)
this.#setStyle()
runWithOwner(this.#reactiveRoot, () => startEffects(this)) // start new-style (decorator) effects
}
disconnectedCallback() {
this.#effects.clearEffects() // Clean up old-style (non-decorator) effects.
stopEffects(this) // Clean up new-style (decorator) effects
this.#disposeRoot?.()
this.#disposeTemplate?.()
this.#cleanupStyle()
}
attributeChangedCallback?(name: string, oldVal: string | null, newVal: string | null): void
static #styleRootNodeRefCountPerTagName = new WeakMap<Node, Record<string, number>>()
#styleRootNode: HTMLHeadElement | ShadowRoot | null = null
#defaultHostStyle = (hostSelector: string) => /*css*/ `${hostSelector} {
display: block;
}`
static #elementId = 0
#id = LumeElement.#elementId++
#dynamicStyle: HTMLStyleElement | null = null
#setStyle() {
const ctor = this.constructor as typeof LumeElement
const staticCSS = typeof ctor.css === 'function' ? (ctor.css = ctor.css()) : ctor.css || ''
const instanceCSS = typeof this.css === 'function' ? this.css() : this.css || ''
if (this.hasShadow) {
const hostSelector = ':host'
const staticStyle = document.createElement('style')
staticStyle.innerHTML = `
${this.#defaultHostStyle(hostSelector)}
${staticCSS}
${instanceCSS}
`
// If this element has a shadow root, put the style there. This is the
// standard way to scope styles to a component.
this.styleRoot.appendChild(staticStyle)
// TODO use adoptedStyleSheets when that is supported by FF and Safari
} else {
// When this element doesn't have a shadow root, then we want to append the
// style only once to the rootNode where it lives (a ShadoowRoot or
// Document). If there are multiple of this same element in the rootNode,
// then the style will be added only once and will style all the elements
// in the same rootNode.
// Because we're connected, getRootNode will return either the
// Document, or a ShadowRoot.
const rootNode = this.getRootNode()
this.#styleRootNode = rootNode === document ? document.head : (rootNode as ShadowRoot)
let refCountPerTagName = LumeElement.#styleRootNodeRefCountPerTagName.get(this.#styleRootNode)
if (!refCountPerTagName)
LumeElement.#styleRootNodeRefCountPerTagName.set(this.#styleRootNode, (refCountPerTagName = {}))
const refCount = refCountPerTagName[this.tagName] || 0
refCountPerTagName[this.tagName] = refCount + 1
if (refCount === 0) {
const hostSelector = this.tagName.toLowerCase()
const staticStyle = document.createElement('style')
staticStyle.innerHTML = `
${this.#defaultHostStyle(hostSelector)}
${staticCSS ? staticCSS.replaceAll(':host', hostSelector) : staticCSS}
`
staticStyle.id = this.tagName.toLowerCase()
this.#styleRootNode.appendChild(staticStyle)
}
if (instanceCSS) {
// For dynamic per-instance styles, make one style element per
// element instance so it contains that element's unique styles,
// associated to a unique attribute selector.
const id = this.tagName.toLowerCase() + '-' + this.#id
// Add the unique attribute that the style selector will target.
this.setAttribute(id, '')
// TODO Instead of creating one style element per custom
// element, we can add the styles to a single style element. We
// can use the CSS OM instead of innerHTML to make it faster
// (but innerHTML is nice for dev mode because it shows the
// content in the DOM when looking in element inspector, so
// allow option for both).
const instanceStyle = (this.#dynamicStyle = document.createElement('style'))
instanceStyle.id = id
instanceStyle.innerHTML = instanceCSS.replaceAll(':host', `[${id}]`)
const rootNode = this.getRootNode()
this.#styleRootNode = rootNode === document ? document.head : (rootNode as ShadowRoot)
this.#styleRootNode.appendChild(instanceStyle)
}
}
}
#cleanupStyle() {
do {
if (this.hasShadow) break
const refCountPerTagName = LumeElement.#styleRootNodeRefCountPerTagName.get(this.#styleRootNode!)
if (!refCountPerTagName) break
let refCount = refCountPerTagName[this.tagName]
if (refCount === undefined) break
refCountPerTagName[this.tagName] = --refCount
if (refCount === 0) {
delete refCountPerTagName[this.tagName]
// TODO PERF Improve performance by saving the style
// instance on a static var, instead of querying for it.
const style = this.#styleRootNode!.querySelector('#' + this.tagName)
style?.remove()
}
} while (false)
if (this.#dynamicStyle) this.#dynamicStyle.remove()
}
// not used currently, but we'll leave this here so that child classes can
// call super, and that way we can add an implementation later when needed.
adoptedCallback() {}
}
// TODO rename the export to LumeElement in a breaking version bump.
export {LumeElement as Element}
export type AttributeHandlerMap = Record<string, AttributeHandler>
import type {JSX} from './jsx-runtime.js'
type JSXOrDOM = JSX.Element | globalThis.Element
type TemplateContent = JSXOrDOM | JSXOrDOM[]
type Template = TemplateContent | (() => TemplateContent)
// prettier-ignore
/**
* A helper for defining the JSX types of an element's attributes.
*
* You give it your element class and a list of properties (a string
* union type), and it outputs a type with those properties being
* optional and dash-cased. The output object also contains all the
* built-in HTML attributes. You can then augment the
* JSX.IntrinsicElements definition with the attributes for your element.
*
* For example, you would do the following so that your element's attribute
* are available and type checked in the JSX of any consumers:
*
* ```js
* import {Element, attribute, numberAttribute, element, ElementAttributes} from '@lume/element'
*
* @element('cool-element')
* class CoolElement extends Element {
* @attribute foo: string | null = null
* @attribute bar: string | null = 'bar'
* @numberAttribute(123) loremIpsum = 123
* }
*
* declare module 'solid-js' {
* namespace JSX {
* interface IntrinsicElements {
* 'cool-element': ElementAttributes<CoolElement, 'foo' | 'bar'>
* }
* }
* }
* ```
*
* The result is that TypeScript will properly type-check the following JSX
* expression (notice loremIpsum is camelCase in the class, but dash-cased
* lorem-ipsum is used in the JSX):
*
* ```jsx
* let coolEl = <cool-element foo={'foo'} bar={null} lorem-ipsum={456}></cool-element>
* ```
*
* If your element has no attributes, you can use `ElementAttributes<YourElement>`
* without specifying any properties.
*/
export type ElementAttributes<
El,
// The `keyof {}` type here allows SelectedProperties to be omitted, picking no properties. For example `ElementAttributes<MyElement>`.
SelectedProperties extends keyof RemoveSetterPrefixes<RemoveAccessors<El>> | keyof {} = keyof {},
AdditionalProperties extends object = {},
> =
// Any props inherited from HTMLElement except for any that we will override from the custom element subclass or AdditionalProperties
& Omit<
JSX.HTMLAttributes<El>,
SelectedProperties | keyof AdditionalProperties | 'onerror'
>
// Fixes the onerror JSX prop type (https://github.com/microsoft/TypeScript-DOM-lib-generator/issues/1821)
& { onerror?: ((error: ErrorEvent) => void) | null }
// All non-'on' non-boolean non-number properties
& Partial< DashCasedProps< WithStringValues< NonNumberProps< NonBooleanProps< NonOnProps<El, SelectedProperties> > > > > >
// All non-boolean non-number properties, prefixed with prop:
& Partial< PrefixProps<'prop:', WithStringValues< NonNumberProps< NonBooleanProps< Pick<RemapSetters<El>, SelectedProperties> > > > > >
// All non-boolean non-number properties, prefixed with attr:
& Partial< PrefixProps<'attr:', DashCasedProps< AsStringValues< NonNumberProps< NonBooleanProps< Pick<RemapSetters<El>, SelectedProperties> > > > > > >
// Boolean attributes
& Partial<
DashCasedProps< WithBooleanStringValues< BooleanProps< NonOnProps<El, SelectedProperties> > > >
& PrefixProps<'bool:', DashCasedProps< AsBooleanValues< BooleanProps< Pick<RemapSetters<El>, SelectedProperties> > > > >
& PrefixProps<'prop:', WithBooleanStringValues< BooleanProps< Pick<RemapSetters<El>, SelectedProperties> > > >
& PrefixProps<'attr:', DashCasedProps< WithBooleanStringValues< BooleanProps< Pick<RemapSetters<El>, SelectedProperties> > > > >
>
// Number attributes
& Partial<
DashCasedProps< WithNumberStringValues< NumberProps< NonOnProps<El, SelectedProperties> > > >
& PrefixProps<'prop:', WithNumberStringValues< NumberProps< Pick<RemapSetters<El>, SelectedProperties> > > >
& PrefixProps<'attr:', DashCasedProps< WithNumberStringValues< NumberProps< Pick<RemapSetters<El>, SelectedProperties> > > > >
>
// Pick the `on*` event handler types from the element type, without string values (only functions)
& Partial<WithStringValues<SkipUppercase<EventListenersOnly<EventProps<El, SelectedProperties>>>>>
// Also map `on*` event handler types to `on:*` prop types for JSX
& Partial<AddDelimitersToEventKeys<EventListenersOnly<EventProps<El, SelectedProperties>>>>
& AdditionalProperties
// This maps __set__foo, replacing any existing `get foo` type, so that the JSX prop type will be that of the specified setter type instead of the getter type.
export type RemapSetters<El> = RemoveSetterPrefixes<RemoveAccessors<El>>
export type NonOnProps<El, K extends keyof RemapSetters<El>> = Pick<RemapSetters<El>, OmitFromUnion<K, EventKeys<K>>>
export type NonEventProps<El, K extends keyof RemoveSetterPrefixes<RemoveAccessors<El>>> =
// All non-'on' properties
NonOnProps<El, K> &
// Restore 'on' properties that are not functions
NonFunctionsOnly<EventProps<El, K>>
export type EventProps<T, Keys extends keyof T> = Pick<T, EventKeys<OmitFromUnion<Keys, symbol | number>>>
export type NonBooleanProps<T> = Omit<T, keyof BooleanProps<T>>
export type BooleanProps<T> = {[K in keyof T as T[K] extends boolean | 'true' | 'false' ? K : never]: T[K]}
export type NonNumberProps<T> = Omit<T, keyof NumberProps<T>>
export type NumberProps<T> = {[K in keyof T as T[K] extends number ? K : never]: T[K]}
export type FunctionsOnly<T> = {[K in keyof T as NonNullable<T[K]> extends (...args: any[]) => any ? K : never]: T[K]}
export type EventListenersOnly<T> = {[K in keyof T as EventListener extends NonNullable<T[K]> ? K : never]: T[K]}
export type NonFunctionsOnly<T> = {
[K in keyof T as ((...args: any[]) => any) extends NonNullable<T[K]> ? never : K]: T[K]
}
/**
* Make all non-string properties union with |string because they can all
* receive string values from string attributes like opacity="0.5" (those values
* are converted to the types of values they should be, f.e. reading a
* `@numberAttribute` property always returns a `number`)
*/
export type WithStringValues<T extends object> = {
[K in keyof T]: PickFromUnion<T[K], string> extends never
? // if the type does not include a type assignable to string
T[K] | string
: // otherwise it does
T[K]
}
export type WithBooleanStringValues<T extends object> = {[K in keyof T]: T[K] | 'true' | 'false'}
export type AsBooleanValues<T extends object> = {[K in keyof T]: boolean}
export type WithNumberStringValues<T extends object> = {[K in keyof T]: T[K] | `${number}`}
export type AsValues<T extends object, V> = {[K in keyof T]: V}
export type AsStringValues<T extends object> = {
[K in keyof T]: PickFromUnion<T[K], string> extends never
? // if the type does not include a type assignable to string
string
: // otherwise it does
T[K]
}
type StringKeysOnly<T extends PropertyKey> = OmitFromUnion<T, number | symbol>
// Given a union, omit any types that extend the given type from the union.
export type OmitFromUnion<T, TypeToOmit> = T extends TypeToOmit ? never : T
// Given a union, pick any types that extend the given type from the union.
type PickFromUnion<T, TypeToPick> = T extends TypeToPick ? T : never
// export type EventKeys<T extends string> = T extends `on${infer _}` ? T : never
export type EventKeys<T extends string> = T extends `on${string}` ? T : never
export type AddDelimitersToEventKeys<T extends object> = {
[K in keyof T as K extends string ? AddDelimiters<K, ':'> : never]: T[K]
}
type AddDelimiters<T extends string, Delimiter extends string> = T extends `${'on'}${infer Right}`
? `${'on'}${Delimiter}${Right}`
: T
export type PrefixProps<Prefix extends string, T> = {[K in keyof T as K extends string ? `${Prefix}${K}` : K]: T[K]}
export type RemoveSetterPrefixes<T> = RemovePrefixes<T, SetterTypePrefix>
export type RemovePrefixes<T, Prefix extends string> = {
[K in keyof T as K extends string ? RemovePrefix<K, Prefix> : K]: T[K]
}
type RemovePrefix<T extends string, Prefix extends string> = T extends `${Prefix}${infer Rest}` ? Rest : T
export type RemoveAccessors<T> = {
[K in keyof T as K extends RemovePrefix<StringKeysOnly<SetterTypeKeysFor<T>>, SetterTypePrefix> ? never : K]: T[K]
}
type SetterTypeKeysFor<T> = keyof PrefixPick<T, SetterTypePrefix>
type PrefixPick<T, Prefix extends string> = {[K in keyof T as K extends `${Prefix}${string}` ? K : never]: T[K]}
export type SetterTypePrefix = '__set__'
export type ContainsUppercase<S extends string> = S extends `${infer First}${infer Rest}`
? First extends Uppercase<First>
? true
: ContainsUppercase<Rest>
: false
export type SkipUppercase<T> = {
[K in keyof T as K extends string ? (ContainsUppercase<K> extends true ? never : K) : never]: T[K]
}