diff --git a/.changeset/light-jobs-retire.md b/.changeset/light-jobs-retire.md new file mode 100644 index 0000000000..7c9f747b07 --- /dev/null +++ b/.changeset/light-jobs-retire.md @@ -0,0 +1,5 @@ +--- +'@lit-labs/react': patch +--- + +Event callbacks can be typed by casting with EventHandler diff --git a/packages/labs/react/README.md b/packages/labs/react/README.md index 1a89e72eee..247d034e82 100644 --- a/packages/labs/react/README.md +++ b/packages/labs/react/README.md @@ -55,6 +55,51 @@ React component. /> ``` +#### Typescript + +Event callback types can be refined by type casting with `EventName`. The +type cast helps `createComponent` correlate typed callbacks to property names in +the event property map. + +Non-casted event names will fallback to an event type of `Event`. + +```ts +import type {EventName} from '@lit-labs/react'; + +import * as React from 'react'; +import {createComponent} from '@lit-labs/react'; +import {MyElement} from './my-element.js'; + +export const MyElementComponent = createComponent( + React, + 'my-element', + MyElement, + { + onClick: 'pointerdown' as EventName, + onChange: 'input', + } +); +``` + +Event callbacks will match their type cast. In the example below, a +`PointerEvent` is expected in the `onClick` callback. + +```tsx + { + console.log('DOM PointerEvent called!'); + }} + onChange={(e: Event) => { + console.log(e); + }} +/> +``` + +NOTE: This type casting is not associated to any component property. Be +careful to use the corresponding type dispatched or bubbled from the +webcomponent. Incorrect types might result in additional properties, missing +properties, or properties of the wrong type. + ## `useController` Reactive Controllers allow developers to hook a component's lifecycle to bundle diff --git a/packages/labs/react/src/create-component.ts b/packages/labs/react/src/create-component.ts index 2164c74787..2f79f65fd7 100644 --- a/packages/labs/react/src/create-component.ts +++ b/packages/labs/react/src/create-component.ts @@ -84,16 +84,28 @@ const setRef = (ref: React.Ref, value: Element | null) => { } }; -type Events = { - [P in keyof S]?: (e: Event) => unknown; -}; - type StringValued = { [P in keyof T]: string; }; type Constructor = {new (): T}; +/*** + * Typecast that curries an Event type through a string. The goal of the type + * cast is to match a prop name to a typed event callback. + */ +export type EventName = string & { + __event_type: T; +}; + +type Events = Record; + +type EventProps = { + [K in keyof R]: R[K] extends EventName + ? (e: R[K]['__event_type']) => void + : (e: Event) => void; +}; + /** * Creates a React component for a custom element. Properties are distinguished * from attributes automatically, and events can be configured so they are @@ -115,11 +127,11 @@ type Constructor = {new (): T}; * messages. Default value is inferred from the name of custom element class * registered via `customElements.define`. */ -export const createComponent = ( +export const createComponent = ( React: typeof ReactModule, tagName: string, elementClass: Constructor, - events?: StringValued, + events?: E, displayName?: string ) => { const Component = React.Component; @@ -132,8 +144,8 @@ export const createComponent = ( type UserProps = React.PropsWithChildren< React.PropsWithRef< Partial> & - Events & - React.HTMLAttributes + Partial> & + Omit, keyof E> > >; diff --git a/packages/labs/react/src/test/create-component_test.tsx b/packages/labs/react/src/test/create-component_test.tsx index 95cba0581e..24336d1fe0 100644 --- a/packages/labs/react/src/test/create-component_test.tsx +++ b/packages/labs/react/src/test/create-component_test.tsx @@ -4,6 +4,8 @@ * SPDX-License-Identifier: BSD-3-Clause */ +import type {EventName} from "../create-component.js"; + import {ReactiveElement} from '@lit/reactive-element'; import {property} from '@lit/reactive-element/decorators/property.js'; import {customElement} from '@lit/reactive-element/decorators/custom-element.js'; @@ -67,7 +69,7 @@ suite('createComponent', () => { }); const basicElementEvents = { - onFoo: 'foo', + onFoo: 'foo' as EventName, onBar: 'bar', }; @@ -241,7 +243,7 @@ suite('createComponent', () => { let fooEvent: Event | undefined, fooEvent2: Event | undefined, barEvent: Event | undefined; - const onFoo = (e: Event) => { + const onFoo = (e: MouseEvent) => { fooEvent = e; }; const onFoo2 = (e: Event) => {