diff --git a/.changeset/eighty-hounds-leave.md b/.changeset/eighty-hounds-leave.md index 0a86c6c9..395f0aa9 100644 --- a/.changeset/eighty-hounds-leave.md +++ b/.changeset/eighty-hounds-leave.md @@ -2,4 +2,4 @@ "@chialab/dna": patch --- -cleanup component class typings +Cleanup component class typings diff --git a/src/Component.ts b/src/Component.ts index 662190f3..2e9584bb 100644 --- a/src/Component.ts +++ b/src/Component.ts @@ -33,11 +33,6 @@ export type WithComponentProto = T & { * We use this mixin to cast the Component class constructor in order to preserve type definition. */ export interface ComponentMixin { - /** - * Type getter for component properties. - */ - readonly __properties__: Props; - /** * Type getter for JSX properties. */ @@ -72,7 +67,7 @@ export interface ComponentMixin { * @param oldValue The previous value of the property. * @param newValue The new value for the property (undefined if removed). */ - stateChangedCallback

(propertyName: P, oldValue: this[P] | undefined, newValue: this[P]): void; + stateChangedCallback

(propertyName: P, oldValue: this[P] | undefined, newValue: this[P]): void; /** * Invoked each time one of a Component's property is setted, removed, or changed. @@ -81,7 +76,7 @@ export interface ComponentMixin { * @param oldValue The previous value of the property. * @param newValue The new value for the property (undefined if removed). */ - propertyChangedCallback

(propertyName: P, oldValue: this[P] | undefined, newValue: this[P]): void; + propertyChangedCallback

(propertyName: P, oldValue: this[P] | undefined, newValue: this[P]): void; /** * Get the inner value of a property. @@ -89,7 +84,7 @@ export interface ComponentMixin { * @param propertyName The name of the property to get. * @returns The inner value of the property. */ - getInnerPropertyValue

(propertyName: P): this[P]; + getInnerPropertyValue

(propertyName: P): this[P]; /** * Set the inner value of a property. @@ -97,7 +92,7 @@ export interface ComponentMixin { * @param propertyName The name of the property to get. * @param value The inner value to set. */ - setInnerPropertyValue

(propertyName: P, value: this[P]): void; + setInnerPropertyValue

(propertyName: P, value: this[P]): void; /** * Observe a Component Property. @@ -105,7 +100,7 @@ export interface ComponentMixin { * @param propertyName The name of the Property to observe * @param observer The callback function */ - observe

(propertyName: P, observer: PropertyObserver): void; + observe

(propertyName: P, observer: PropertyObserver): void; /** * Unobserve a Component Property. @@ -113,7 +108,7 @@ export interface ComponentMixin { * @param propertyName The name of the Property to unobserve * @param observer The callback function to remove */ - unobserve

(propertyName: P, observer: PropertyObserver): void; + unobserve

(propertyName: P, observer: PropertyObserver): void; /** * Dispatch a custom Event. @@ -334,11 +329,6 @@ function initSlotChildNodes(ctor: Constructor) => { const Component = class Component extends (ctor as Constructor) { - /** - * Type getter for component properties. - */ - declare readonly __properties__: Props; - /** * Type getter for JSX properties. */ @@ -544,7 +534,7 @@ const mixin = (ctor: Constructor) => { * @param oldValue The previous value of the property. * @param newValue The new value for the property (undefined if removed). */ - stateChangedCallback

(propertyName: P, oldValue: this[P] | undefined, newValue: this[P]) { + stateChangedCallback

(propertyName: P, oldValue: this[P] | undefined, newValue: this[P]) { reflectPropertyToAttribute(this, propertyName, newValue); } @@ -555,7 +545,7 @@ const mixin = (ctor: Constructor) => { * @param oldValue The previous value of the property. * @param newValue The new value for the property (undefined if removed). */ - propertyChangedCallback

(propertyName: P, oldValue: this[P] | undefined, newValue: this[P]) { + propertyChangedCallback

(propertyName: P, oldValue: this[P] | undefined, newValue: this[P]) { reflectPropertyToAttribute(this, propertyName, newValue); } @@ -565,7 +555,7 @@ const mixin = (ctor: Constructor) => { * @param propertyName The name of the property to get. * @returns The inner value of the property. */ - getInnerPropertyValue

(propertyName: P): this[P] { + getInnerPropertyValue

(propertyName: P): this[P] { const property = getProperty(this, propertyName, true); return this[property.symbol as keyof this] as this[P]; } @@ -576,7 +566,7 @@ const mixin = (ctor: Constructor) => { * @param propertyName The name of the property to get. * @param value The inner value to set. */ - setInnerPropertyValue

(propertyName: P, value: this[P]) { + setInnerPropertyValue

(propertyName: P, value: this[P]) { const property = getProperty(this, propertyName, true); this[property.symbol as keyof this] = value; } @@ -587,7 +577,7 @@ const mixin = (ctor: Constructor) => { * @param propertyName The name of the Property to observe * @param observer The callback function */ - observe

(propertyName: P, observer: PropertyObserver) { + observe

(propertyName: P, observer: PropertyObserver) { addObserver(this, propertyName, observer); } @@ -597,7 +587,7 @@ const mixin = (ctor: Constructor) => { * @param propertyName The name of the Property to unobserve * @param observer The callback function to remove */ - unobserve

(propertyName: P, observer: PropertyObserver) { + unobserve

(propertyName: P, observer: PropertyObserver) { removeObserver(this, propertyName, observer); } diff --git a/src/JSX.ts b/src/JSX.ts index d3f00af6..6d462178 100644 --- a/src/JSX.ts +++ b/src/JSX.ts @@ -30,10 +30,17 @@ export type GetCustomElementsProps = Exclude<{ never; }[keyof JSXInternal.CustomElements], never>; +/** + * Get HTML attributes by prototype. + */ +type ElementAttributes = { + [K in keyof HTMLTagNameMap]: HTMLTagNameMap[K] extends T ? IntrinsicElementAttributes[K] : HTMLAttributes; +}[keyof HTMLTagNameMap]; + /** * Get render attributes set. */ -export type AttributeProperties = Omit; +export type AttributeProperties = T & Omit; /** * Get render properties for keyed nodes. @@ -97,7 +104,7 @@ export type VElement = { type: T; key: unknown; namespace: string; - properties: Props & AttributeProperties & KeyedProperties & TreeProperties & EventProperties & ElementProperties; + properties: AttributeProperties, ElementAttributes> & KeyedProperties & TreeProperties & EventProperties & ElementProperties; children: Template[]; [V_SYM]: true; }; @@ -109,7 +116,7 @@ export type VComponent = { type: T; key?: unknown; namespace?: string; - properties: Props> & AttributeProperties & KeyedProperties & TreeProperties & EventProperties & ElementProperties; + properties: AttributeProperties>, HTMLAttributes> & KeyedProperties & TreeProperties & EventProperties & ElementProperties; children: Template[]; [V_SYM]: true; }; @@ -120,7 +127,7 @@ export type VComponent = { export type VSlot = { type: 'slot'; key: unknown; - properties: AttributeProperties & KeyedProperties & TreeProperties & EventProperties; + properties: AttributeProperties, IntrinsicElementAttributes['slot']> & KeyedProperties & TreeProperties & EventProperties; children: Template[]; [V_SYM]: true; }; @@ -132,7 +139,7 @@ export type VTag = { type: T; key: unknown; namespace: string; - properties: AttributeProperties & KeyedProperties & TreeProperties & EventProperties & ElementProperties; + properties: AttributeProperties : T extends keyof SVGTagNameMap ? Props : Props, IntrinsicElementAttributes[T]> & KeyedProperties & TreeProperties & EventProperties & ElementProperties; children: Template[]; [V_SYM]: true; }; @@ -221,10 +228,10 @@ export const isVTag = (target: VObject): target is VTag(tagOrComponent: T, properties: Parameters[0] & KeyedProperties & TreeProperties, ...children: Template[]): VFunction; -function h(tagOrComponent: T, properties: Props> & HTMLAttributes & KeyedProperties & TreeProperties & EventProperties & ElementProperties, ...children: Template[]): VComponent; -function h(tagOrComponent: T, properties: Props & HTMLAttributes & KeyedProperties & TreeProperties & EventProperties & ElementProperties, ...children: Template[]): VElement; -function h(tagOrComponent: T, properties: Props & IntrinsicElementAttributes[T] & KeyedProperties & TreeProperties & EventProperties & ElementProperties, ...children: Template[]): VTag; -function h(tagOrComponent: T, properties: Props & IntrinsicElementAttributes[T] & KeyedProperties & TreeProperties & EventProperties & ElementProperties, ...children: Template[]): VTag; +function h(tagOrComponent: T, properties: AttributeProperties>, HTMLAttributes> & KeyedProperties & TreeProperties & EventProperties & ElementProperties, ...children: Template[]): VComponent; +function h(tagOrComponent: T, properties: AttributeProperties, ElementAttributes> & KeyedProperties & TreeProperties & EventProperties & ElementProperties, ...children: Template[]): VElement; +function h(tagOrComponent: T, properties: AttributeProperties, IntrinsicElementAttributes[T]> & KeyedProperties & TreeProperties & EventProperties & ElementProperties, ...children: Template[]): VTag; +function h(tagOrComponent: T, properties: AttributeProperties, IntrinsicElementAttributes[T]> & KeyedProperties & TreeProperties & EventProperties & ElementProperties, ...children: Template[]): VTag; /** * Function factory to use as JSX pragma. * @@ -235,7 +242,7 @@ function h(tagOrComponent: T, properties: Props( tagOrComponent: T, - properties: HTMLAttributes & KeyedProperties & TreeProperties & ElementProperties | null = null, + properties: AttributeProperties, HTMLAttributes> & KeyedProperties & TreeProperties & ElementProperties | null = null, ...children: Template[] ) { const { children: propertiesChildren } = (properties || {}) as TreeProperties; @@ -283,10 +290,10 @@ function h(tagOrComponent: T, properties: Parameters[0] & TreeProperties, key?: unknown): VFunction; -function jsx(tagOrComponent: T, properties: Props> & HTMLAttributes & TreeProperties & EventProperties & ElementProperties, key?: unknown): VComponent; -function jsx(tagOrComponent: T, properties: Props & HTMLAttributes & TreeProperties & EventProperties & ElementProperties, key?: unknown): VElement; -function jsx(tagOrComponent: T, properties: Props & IntrinsicElementAttributes[T] & TreeProperties & EventProperties & ElementProperties, key?: unknown): VTag; -function jsx(tagOrComponent: T, properties: Props & IntrinsicElementAttributes[T] & TreeProperties & EventProperties & ElementProperties, key?: unknown): VTag; +function jsx(tagOrComponent: T, properties: AttributeProperties>, HTMLAttributes> & TreeProperties & EventProperties & ElementProperties, key?: unknown): VComponent; +function jsx(tagOrComponent: T, properties: AttributeProperties, ElementAttributes> & TreeProperties & EventProperties & ElementProperties, key?: unknown): VElement; +function jsx(tagOrComponent: T, properties: AttributeProperties, IntrinsicElementAttributes[T]> & TreeProperties & EventProperties & ElementProperties, key?: unknown): VTag; +function jsx(tagOrComponent: T, properties: AttributeProperties, IntrinsicElementAttributes[T]> & TreeProperties & EventProperties & ElementProperties, key?: unknown): VTag; /** * Function factory to use as JSX pragma. * @@ -330,11 +337,11 @@ export namespace JSXInternal { [K in keyof CustomElements]: 'extends' extends keyof JSXInternal.CustomElements[K] ? never : - Props & AttributeProperties & KeyedProperties & TreeProperties & EventProperties & ElementProperties; + AttributeProperties, HTMLAttributes> & KeyedProperties & TreeProperties & EventProperties & ElementProperties; } & { - [K in keyof HTMLTagNameMap]: (({ is?: never } & Props) | GetCustomElementsProps) & AttributeProperties & KeyedProperties & TreeProperties & EventProperties & ElementProperties; + [K in keyof HTMLTagNameMap]: AttributeProperties<(({ is?: never } & Props) | GetCustomElementsProps), IntrinsicElementAttributes[K]> & KeyedProperties & TreeProperties & EventProperties & ElementProperties; } & { - [K in keyof SVGTagNameMap]: Props & AttributeProperties & KeyedProperties & TreeProperties & EventProperties & ElementProperties; + [K in keyof SVGTagNameMap]: AttributeProperties, IntrinsicElementAttributes[K]> & KeyedProperties & TreeProperties & EventProperties & ElementProperties; }; } diff --git a/src/events.ts b/src/events.ts index f1b6eb95..63cfd6c2 100644 --- a/src/events.ts +++ b/src/events.ts @@ -1,5 +1,5 @@ import { type ComponentInstance, type ComponentConstructor } from './Component'; -import { type MethodsOf } from './property'; +import { type Methods } from './property'; import { type Constructor, type ClassElement, HTMLElementConstructor, isElement, isEvent, matchesImpl, createEventImpl, hasOwnProperty, getOwnPropertyDescriptor, getPrototypeOf } from './helpers'; /** @@ -452,7 +452,7 @@ export const defineListeners = (prototype: T) => { * @param methodKey The method name. * @returns The property descriptor. */ -export const createListener = >( +export const createListener = >( targetOrClassElement: T, eventName: string, target: EventTarget | null, @@ -492,7 +492,7 @@ function listen(eventName: string, target: EventTarget, options?: AddEventListen * @returns The decorator initializer. */ function listen(eventName: string, target?: string | EventTarget | AddEventListenerOptions, options?: AddEventListenerOptions) { - return >( + return >( targetOrClassElement: T, methodKey: P ) => createListener( diff --git a/src/property.ts b/src/property.ts index bcccb84c..72af3c1c 100644 --- a/src/property.ts +++ b/src/property.ts @@ -19,7 +19,9 @@ const WATCHED_SYMBOL: unique symbol = Symbol(); /** * Checks types equality. */ -type IfEquals = X extends Y ? Y extends X ? A : B : B; +type IfEquals = + (() => G extends X ? 1 : 2) extends + (() => G extends Y ? 1 : 2) ? A : B; /** * Check if a property is writable. @@ -29,39 +31,39 @@ type IfWritable = IfEquals<{ [Q in K]: T[K] }, { -re /** * Check if a property is a method. */ -type IfMethod = T[K] extends (...args: never) => unknown ? A : B; +type IfMethod = T[K] extends Function ? A : B; /** * Exclude component mixin properties. */ -type NonReservedKeys = Exclude; +type NonReservedKeys = Exclude | keyof ElementCSSInlineStyle>; /** - * Get all defined properties of a component. + * Pick defined properties of a component. */ export type Props = { [K in NonReservedKeys as IfMethod>]?: T[K]; }; /** - * Get all methods of a class. + * Pick methods of a class. */ -export type MethodsOf = { - [K in keyof T]: T[K] extends Function ? (T[K] extends undefined ? never : K) : never; -}[keyof T]; +export type Methods = { + [K in NonReservedKeys as IfMethod]: T[K]; +}; /** * Retrieve properties declarations of a Component. */ export type PropertiesOf = { - [P in keyof T['__properties__']]: Property; + [P in keyof T]: Property; }; /** * Retrieve properties declarations of a Component. */ export type ObserversOf = { - [P in keyof T['__properties__']]: PropertyObserver[]; + [P in keyof T]: PropertyObserver[]; }; /** @@ -180,7 +182,7 @@ export type PropertyConfig = Co /** * A property instance. */ -export type Property = PropertyDescriptor & { +export type Property = PropertyDescriptor & { /** * The property name of the field. */ @@ -287,7 +289,7 @@ export const getProperties = (prototype: WithProper * @returns The property declaration. * @throws If the property is not defined and `failIfMissing` is `true`. */ -export const getProperty = (prototype: T, propertyKey: P, failIfMissing = false) => { +export const getProperty = (prototype: T, propertyKey: P, failIfMissing = false) => { const property = getProperties(prototype)[propertyKey]; if (failIfMissing && !property) { throw new Error(`Missing property ${String(propertyKey)}`); @@ -300,7 +302,7 @@ export const getProperty = (declaration: PropertyDeclaration>) => { +const extractTypes = (declaration: PropertyDeclaration>) => { const type = declaration.type; if (!type) { return []; @@ -329,7 +331,7 @@ export const getWatched = (element: WithProperties< * @param isStatic The property definition is static. * @returns The final descriptor. */ -export const defineProperty = (prototype: T, propertyKey: P, declaration: PropertyDeclaration>, symbolKey: symbol, isStatic = false): PropertyDescriptor => { +export const defineProperty = (prototype: T, propertyKey: P, declaration: PropertyDeclaration>, symbolKey: symbol, isStatic = false): PropertyDescriptor => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const symbol: unique symbol = symbolKey as any; const hasAttribute = declaration.attribute || (declaration.attribute == null ? !declaration.state : false); @@ -504,19 +506,19 @@ export const defineProperties = (prototype: T) => { const propertiesDescriptor = getOwnPropertyDescriptor(ctr, 'properties'); if (propertiesDescriptor) { const descriptorProperties = (propertiesDescriptor.get ? (propertiesDescriptor.get.call(constructor) || {}) : propertiesDescriptor.value) as { - [P in keyof T['__properties__']]: PropertyConfig>; + [P in keyof T]: PropertyConfig>; }; for (const propertyKey in descriptorProperties) { if (propertyKey in handled) { continue; } - const config = descriptorProperties[propertyKey as keyof T['__properties__']]; - const declaration = (typeof config === 'function' || isArray(config) ? { type: config } : config) as PropertyDeclaration>; + const config = descriptorProperties[propertyKey as keyof T]; + const declaration = (typeof config === 'function' || isArray(config) ? { type: config } : config) as PropertyDeclaration>; // eslint-disable-next-line @typescript-eslint/no-explicit-any const symbol: unique symbol = (declaration.symbol as any) || Symbol(propertyKey as string); defineProperty( prototype, - propertyKey as keyof T['__properties__'], + propertyKey as keyof T, declaration, symbol, true @@ -538,7 +540,7 @@ export const defineProperties = (prototype: T) => { export const getPropertyForAttribute = (prototype: T, attributeName: string) => { const properties = getProperties(prototype); for (const propertyKey in properties) { - const property = properties[propertyKey as keyof T['__properties__']]; + const property = properties[propertyKey as keyof T]; if (property.attribute === attributeName) { return property; } @@ -553,7 +555,7 @@ export const getPropertyForAttribute = (prototype: * @param propertyName The name of the changed property. * @param newValue The new value for the property (undefined if removed). */ -export const reflectPropertyToAttribute = (element: T, propertyName: P, newValue: T[P]) => { +export const reflectPropertyToAttribute = (element: T, propertyName: P, newValue: T[P]) => { const property = getProperty(element, propertyName, true); const { attribute, toAttribute } = property; if (attribute && toAttribute) { @@ -610,7 +612,7 @@ const assignFromDescriptor = (declaration: PropertyDeclaration, descriptor: Prop * @returns The property descriptor. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export const createProperty = (targetOrClassElement: T, declaration: PropertyDeclaration>, propertyKey?: P, descriptor?: PropertyDeclaration>): any => { +export const createProperty = (targetOrClassElement: T, declaration: PropertyDeclaration>, propertyKey?: P, descriptor?: PropertyDeclaration>): any => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const symbol: unique symbol = declaration.symbol || Symbol(propertyKey as string) as any; if (propertyKey !== undefined) { @@ -662,7 +664,7 @@ export const createProperty = >(targetOrClassElement: T, propertyKey: P, methodKey?: M): any => { +export const createObserver = >(targetOrClassElement: T, propertyKey: P, methodKey?: M): any => { if (methodKey !== undefined) { addObserver(targetOrClassElement, propertyKey, targetOrClassElement[methodKey] as unknown as PropertyObserver); return; @@ -705,7 +707,7 @@ export const getObservers = (element: WithPropertie * @returns A list of observers. * @throws If the property is not defined. */ -export const getPropertyObservers = (element: T, propertyName: P) => { +export const getPropertyObservers = (element: T, propertyName: P) => { if (!getProperty(element, propertyName)) { throw new Error(`Missing property ${String(propertyName)}`); } @@ -719,7 +721,7 @@ export const getPropertyObservers = (element: T, propertyName: P, observer: PropertyObserver) => { +export const addObserver = (element: T, propertyName: P, observer: PropertyObserver) => { getPropertyObservers(element, propertyName).push(observer); }; @@ -729,7 +731,7 @@ export const addObserver = (element: T, propertyName: P, observer: PropertyObserver) => { +export const removeObserver = (element: T, propertyName: P, observer: PropertyObserver) => { const observers = getPropertyObservers(element, propertyName); const io = observers.indexOf(observer); if (io !== -1) { @@ -743,7 +745,7 @@ export const removeObserver = = Constructor>(declaration: PropertyDeclaration = {}) { - return ( + return ( targetOrClassElement: T, propertyKey?: P, descriptor?: PropertyDeclaration> @@ -756,7 +758,7 @@ export function property = Cons * @returns The decorator initializer. */ export function state = Constructor>(declaration: PropertyDeclaration = {}) { - return ( + return ( targetOrClassElement: T, propertyKey?: P, descriptor?: PropertyDeclaration> @@ -770,7 +772,7 @@ export function state = Constru * @returns The decorator initializer. */ export function observe(propertyKey: string): Function { - return >( + return >( targetOrClassElement: T, methodKey?: P ) => createObserver(targetOrClassElement, propertyKey as keyof PropertiesOf, methodKey); diff --git a/test/typings/Component.ts b/test/typings/Component.ts index 4fb47187..b8445b0d 100644 --- a/test/typings/Component.ts +++ b/test/typings/Component.ts @@ -1,9 +1,13 @@ // eslint-disable-next-line import/no-unresolved -import { window, Component, customElement, extend } from '@chialab/dna'; +import { window, Component, customElement, extend, property } from '@chialab/dna'; @customElement('x-test') export class TestElement extends Component { - readonly active?: boolean; + active?: boolean; + + @property({ + type: Number, + }) width: number = 2; get computed(): string { return this.getInnerPropertyValue('computed');