From babd555082c7c8d213f7f3537d2beb1a5dbb63de Mon Sep 17 00:00:00 2001 From: Joe Pea Date: Mon, 7 Oct 2024 21:33:57 -0700 Subject: [PATCH] **feat:** improve the `ElementAttributes` and `ReactElementAttributes` JSX type helpers. This improves JSX types for Custom Elements in Solid JSX, React JSX, and Preact JSX, especially in React/Preact JSX whereas previously the React/Preact JSX prop types only accepted string values for dash-cased attributes. If you have a `get`ter/`set`ter property in your element class, you can now define a dummy property prefixed with `__set__` to specify the type of the `set`ter, and this will be picked up and lead to improved types in JSX. For example, you can start using like so: Before: ```js @element('some-element') class SomeElement extends Element { @attribute get someProp(): number {...} @attribute set someProp(n: number | 'foo' | 'bar') {...} } declare module 'react' { namespace JSX { interface IntrinsicElements { 'some-element': ReactElementAttributes } } } ``` and this JSX would have a type error: ```jsx return // Error: string is not assignable to number ``` After: ```js @element('some-element') class SomeElement extends Element { @attribute get someProp(): number {...} @attribute set someProp(n: this['__set__someProp']) {...} /** don't use this property, it is for JSX types. */ __set__someProp!: number | 'foo' | 'bar' } // ... the same React JSX definition as before ... ``` and now JSX prop types will allow setting the *setter* types: ```jsx return // No error, yay! ``` Note, the property is camelCase instead of dash-case now. **BREAKING:** This may introduce type errors into existing JSX templates, tested with React 19 (not tested with React 18 or below), but it is an inevitable upgrade for the better. To migrate, there's likely nothing to do in Solid JSX, but in React JSX the selected properties are no longer converted to dash-case, so you'll want to use the original JS property names in React JSX templates. For example this, ```jsx return ``` becomes ```jsx return ``` If you have any issues, please reach out on GitHub or Discord! https://discord.gg/VmvkFcWrsx Co-authored-by: bigmistqke --- README.md | 70 +++++++++++++++++++++++++----- dist/LumeElement.d.ts | 24 ++++++++-- dist/LumeElement.d.ts.map | 2 +- dist/jsx-types-react.test.d.ts | 17 ++++++++ dist/jsx-types-react.test.d.ts.map | 1 + dist/jsx-types-react.test.jsx | 32 ++++++++++++++ dist/jsx-types-react.test.jsx.map | 1 + dist/jsx-types-solid.test.d.ts | 17 ++++++++ dist/jsx-types-solid.test.d.ts.map | 1 + dist/jsx-types-solid.test.jsx | 32 ++++++++++++++ dist/jsx-types-solid.test.jsx.map | 1 + dist/react.d.ts | 11 +++-- dist/react.d.ts.map | 2 +- package.json | 7 ++- src/LumeElement.ts | 63 +++++++++++++++++++++++---- src/jsx-types-react.test.tsx | 44 +++++++++++++++++++ src/jsx-types-solid.test.tsx | 44 +++++++++++++++++++ src/react.ts | 30 ++++++++----- 18 files changed, 356 insertions(+), 43 deletions(-) create mode 100644 dist/jsx-types-react.test.d.ts create mode 100644 dist/jsx-types-react.test.d.ts.map create mode 100644 dist/jsx-types-react.test.jsx create mode 100644 dist/jsx-types-react.test.jsx.map create mode 100644 dist/jsx-types-solid.test.d.ts create mode 100644 dist/jsx-types-solid.test.d.ts.map create mode 100644 dist/jsx-types-solid.test.jsx create mode 100644 dist/jsx-types-solid.test.jsx.map create mode 100644 src/jsx-types-react.test.tsx create mode 100644 src/jsx-types-solid.test.tsx diff --git a/README.md b/README.md index 4557770..b8a2fbd 100644 --- a/README.md +++ b/README.md @@ -237,7 +237,7 @@ npm install --save-dev @babel/cli @babel/core @babel/plugin-proposal-decorators If using TypeScript, set `allowJs` in `tsconfig.json` to allow compiling JS files, f.e.: -```json +```js { "compilerOptions": { "allowJs": true, @@ -247,7 +247,8 @@ If using TypeScript, set `allowJs` in `tsconfig.json` to allow compiling JS file } ``` -and running `npx tsc`. +and running `npx tsc`. See the [TypeScript](#typescript) section below for configuring JSX +types for various frameworks (Solid, React, Preact, etc). If using Babel, add the decorators plugin to `.babelrc`, f.e. @@ -581,9 +582,13 @@ Load the required JSX types in one of two ways: project, but if you have files with different types of JSX, you'll want to use option 1 instead). - ```json + ```js { "compilerOptions": { + /* Solid.js Config */ + // Note, you need to use an additional tool such as Babel, Vite, etc, to + // compile Solid JSX. `npm create solid` will scaffold things for you. + "jsx": "preserve", "jsxImportSource": "solid-js" } } @@ -687,8 +692,10 @@ const el2 = (...) as any as HTMLDivElement #### Type definitions for custom elements +### With Solid JSX + To give your Custom Elements type checking for use with DOM APIs, and type -checking in JSX, use the following template. +checking in Solid JSX, we can add the element type definition to `JSX.IntrinsicElements`: ```tsx /* @jsxImportSource solid-js */ @@ -697,11 +704,9 @@ checking in JSX, use the following template. // anywhere in non-JSX parts of the code, you also need to import it from // solid-js: import {Element, element, stringAttribute, numberAttribute, /*...,*/ JSX} from 'solid-js' -// ^ We imported JSX so that... // Define the attributes that your element accepts export interface CoolElementAttributes extends JSX.HTMLAttributes { - // ^ ...we can use it in this non-JSX code. 'cool-type'?: 'beans' | 'hair' 'cool-factor'?: number // ^ NOTE: These should be dash-case versions of your class's attribute properties. @@ -777,14 +782,27 @@ return ( Defining the types of custom elements for React JSX is similar as for Solid JSX above, but with some small differences for React JSX: +```js +// tsconfig.json +{ + "compilerOptions": { + /* React Config */ + "jsx": "react-jsx", + "jsxImportSource": "react" // React >=19 (Omit for React <=18) + } +} +``` + ```ts import type {HTMLAttributes} from 'react' // Define the attributes that your element accepts, almost the same as before: export interface CoolElementAttributes extends HTMLAttributes { - 'cool-type'?: 'beans' | 'hair' - 'cool-factor'?: number - // ^ NOTE: These should be dash-case versions of your class's attribute properties. + coolType?: 'beans' | 'hair' + coolFactor?: number + // ^ NOTE: These are the names of the class's properties verbatim, not + // dash-cased as with Solid. React works differently than Solid's: it will + // map the exact prop name to the JS property. } // Add your element to the list of known HTML elements, like before. @@ -812,7 +830,7 @@ declare global { > attribute types: ```ts -import type {ReactElementAttributes} from '@lume/element/src/react' +import type {ReactElementAttributes} from '@lume/element/dist/react' // This definition is now shorter than before, and automatically maps the property names to dash-case. export type CoolElementAttributes = ReactElementAttributes @@ -827,6 +845,17 @@ declare global { } ``` +Now when you use `` in React JSX, it will be type checked: + +```jsx +return ( + +) +``` + > [!Note] > You may want to define React JSX types for your elements in separate files, and > have only React users import those files if they need the types, and similar if you make @@ -834,6 +863,27 @@ declare global { > yet, but you can manually augment JSX as in the examples above on a > per-framework basis, contributions welcome!). +### With Preact JSX + +It works the same as the previous section for React JSX. Define the element +types with the same `ReactElementAttributes` helper as described above. In your +TypeScript `compilerOptions` make sure you link to the React compatibility +layer: + +```json +{ + "compilerOptions": { + /* Preact Config */ + "jsx": "react-jsx", + "jsxImportSource": "preact", + "paths": { + "react": ["./node_modules/preact/compat/"], + "react-dom": ["./node_modules/preact/compat/"] + } + } +} +``` + ## API ### `Element` diff --git a/dist/LumeElement.d.ts b/dist/LumeElement.d.ts index a6ccb30..868ee48 100644 --- a/dist/LumeElement.d.ts +++ b/dist/LumeElement.d.ts @@ -204,14 +204,32 @@ type Template = TemplateContent | (() => TemplateContent); * let coolEl = * ``` */ -export type ElementAttributes = WithStringValues>>> & AdditionalProperties & Omit, SelectedProperties | keyof AdditionalProperties>; +export type ElementAttributes, SetterTypePrefix>, AdditionalProperties extends object = {}> = Omit, SelectedProperties | keyof AdditionalProperties | 'onerror'> & { + onerror?: ((error: ErrorEvent) => void) | null; +} & Partial, SetterTypePrefix>, SelectedProperties>>>> & AdditionalProperties; /** * 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`) */ -type WithStringValues = { - [Property in keyof Type]: NonNullable extends string ? Type[Property] : Type[Property] | string; +export type WithStringValues = { + [Property in keyof Type]: PickFromUnion extends never ? // if the type does not include a type assignable to string + Type[Property] | string : Type[Property]; }; +type StringKeysOnly = OmitFromUnion; +type OmitFromUnion = T extends TypeToOmit ? never : T; +type PickFromUnion = T extends TypeToPick ? T : never; +export type RemovePrefixes = { + [K in keyof T as K extends string ? RemovePrefix : K]: T[K]; +}; +type RemovePrefix = T extends `${Prefix}${infer Rest}` ? Rest : T; +export type RemoveAccessors = { + [K in keyof T as K extends RemovePrefix>, SetterTypePrefix> ? never : K]: T[K]; +}; +type SetterTypeKeysFor = keyof PrefixPick; +type PrefixPick = { + [K in keyof T as K extends `${Prefix}${string}` ? K : never]: T[K]; +}; +export type SetterTypePrefix = '__set__'; //# sourceMappingURL=LumeElement.d.ts.map \ No newline at end of file diff --git a/dist/LumeElement.d.ts.map b/dist/LumeElement.d.ts.map index 48391c1..25b7cc4 100644 --- a/dist/LumeElement.d.ts.map +++ b/dist/LumeElement.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"LumeElement.d.ts","sourceRoot":"","sources":["../src/LumeElement.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"LumeElement.d.ts","sourceRoot":"","sources":["../src/LumeElement.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/jsx-types-react.test.d.ts b/dist/jsx-types-react.test.d.ts new file mode 100644 index 0000000..135e9f6 --- /dev/null +++ b/dist/jsx-types-react.test.d.ts @@ -0,0 +1,17 @@ +import type { ReactElementAttributes } from './react.js'; +declare class SomeElement extends HTMLElement { + someProp: 'true' | 'false' | boolean; + get otherProp(): number; + set otherProp(_: this['__set__otherProp']); + /** do not use this property, its only for JSX types */ + __set__otherProp: number | 'foo'; +} +declare module 'react' { + namespace JSX { + interface IntrinsicElements { + 'some-element': ReactElementAttributes; + } + } +} +export {}; +//# sourceMappingURL=jsx-types-react.test.d.ts.map \ No newline at end of file diff --git a/dist/jsx-types-react.test.d.ts.map b/dist/jsx-types-react.test.d.ts.map new file mode 100644 index 0000000..2f39cb6 --- /dev/null +++ b/dist/jsx-types-react.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"jsx-types-react.test.d.ts","sourceRoot":"","sources":["../src/jsx-types-react.test.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,sBAAsB,EAAC,MAAM,YAAY,CAAA;AAEtD,cAAM,WAAY,SAAQ,WAAW;IACpC,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CAAO;IAE3C,IAAI,SAAS,IAAI,MAAM,CAEtB;IACD,IAAI,SAAS,CAAC,CAAC,EAAE,IAAI,CAAC,kBAAkB,CAAC,EAAI;IAE7C,uDAAuD;IACvD,gBAAgB,EAAG,MAAM,GAAG,KAAK,CAAA;CACjC;AAID,OAAO,QAAQ,OAAO,CAAC;IACtB,UAAU,GAAG,CAAC;QACb,UAAU,iBAAiB;YAC1B,cAAc,EAAE,sBAAsB,CAAC,WAAW,EAAE,UAAU,GAAG,WAAW,CAAC,CAAA;SAC7E;KACD;CACD"} \ No newline at end of file diff --git a/dist/jsx-types-react.test.jsx b/dist/jsx-types-react.test.jsx new file mode 100644 index 0000000..633c3a6 --- /dev/null +++ b/dist/jsx-types-react.test.jsx @@ -0,0 +1,32 @@ +/* @jsxImportSource react */ +class SomeElement extends HTMLElement { + someProp = true; + get otherProp() { + return 0; + } + set otherProp(_) { } + /** do not use this property, its only for JSX types */ + __set__otherProp; +} +SomeElement; +describe('JSX types with ReactElementAttributes', () => { + it('derives JSX types from classes', () => { + ; + <> + + + + {/* @ts-expect-error good, number is invalid */} + + {/* @ts-expect-error good, 'blah' is invalid */} + + + {/* Additionally TypeScript will allow unknown dash-case props (as we didn't not define JS properties with these exact dash-cased names, React 19+ will set the element attributes, useful for setting the attributes but React has no way to specify to set attributes for names without dashes) */} + + {/* @ts-expect-error foo doesn't exist. TypeScript will only check existence of properties without dashes */} + + ; + }); +}); +export {}; +//# sourceMappingURL=jsx-types-react.test.jsx.map \ No newline at end of file diff --git a/dist/jsx-types-react.test.jsx.map b/dist/jsx-types-react.test.jsx.map new file mode 100644 index 0000000..e2fb448 --- /dev/null +++ b/dist/jsx-types-react.test.jsx.map @@ -0,0 +1 @@ +{"version":3,"file":"jsx-types-react.test.jsx","sourceRoot":"","sources":["../src/jsx-types-react.test.tsx"],"names":[],"mappings":"AAAA,4BAA4B;AAI5B,MAAM,WAAY,SAAQ,WAAW;IACpC,QAAQ,GAA+B,IAAI,CAAA;IAE3C,IAAI,SAAS;QACZ,OAAO,CAAC,CAAA;IACT,CAAC;IACD,IAAI,SAAS,CAAC,CAA2B,IAAG,CAAC;IAE7C,uDAAuD;IACvD,gBAAgB,CAAiB;CACjC;AAED,WAAW,CAAA;AAUX,QAAQ,CAAC,uCAAuC,EAAE,GAAG,EAAE;IACtD,EAAE,CAAC,gCAAgC,EAAE,GAAG,EAAE;QACzC,CAAC;QAAA,EACA;GAAA,CAAC,YAAY,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC,KAAK,EAC9C;GAAA,CAAC,YAAY,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC,KAAK,EAC9C;GAAA,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,EAC9C;GAAA,CAAC,8CAA8C,CAC/C;GAAA,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,EAC5B;GAAA,CAAC,8CAA8C,CAC/C;GAAA,CAAC,YAAY,CAAC,SAAS,CAAC,MAAM,EAE9B;;GAAA,CAAC,mSAAmS,CACpS;GAAA,CAAC,YAAY,CAAC,SAAS,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,EAChD;GAAA,CAAC,2GAA2G,CAC5G;GAAA,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAC1B;EAAA,GAAG,CAAA;IACJ,CAAC,CAAC,CAAA;AACH,CAAC,CAAC,CAAA"} \ No newline at end of file diff --git a/dist/jsx-types-solid.test.d.ts b/dist/jsx-types-solid.test.d.ts new file mode 100644 index 0000000..3df1786 --- /dev/null +++ b/dist/jsx-types-solid.test.d.ts @@ -0,0 +1,17 @@ +import type { ElementAttributes } from './LumeElement.js'; +declare class SomeElement extends HTMLElement { + someProp: 'true' | 'false' | boolean; + get otherProp(): number; + set otherProp(_: this['__set__otherProp']); + /** do not use this property, its only for JSX types */ + __set__otherProp: number | 'foo'; +} +declare module 'solid-js' { + namespace JSX { + interface IntrinsicElements { + 'some-element': ElementAttributes; + } + } +} +export {}; +//# sourceMappingURL=jsx-types-solid.test.d.ts.map \ No newline at end of file diff --git a/dist/jsx-types-solid.test.d.ts.map b/dist/jsx-types-solid.test.d.ts.map new file mode 100644 index 0000000..7e97015 --- /dev/null +++ b/dist/jsx-types-solid.test.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"jsx-types-solid.test.d.ts","sourceRoot":"","sources":["../src/jsx-types-solid.test.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,kBAAkB,CAAA;AAEvD,cAAM,WAAY,SAAQ,WAAW;IACpC,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CAAO;IAE3C,IAAI,SAAS,IAAI,MAAM,CAEtB;IACD,IAAI,SAAS,CAAC,CAAC,EAAE,IAAI,CAAC,kBAAkB,CAAC,EAAI;IAE7C,uDAAuD;IACvD,gBAAgB,EAAG,MAAM,GAAG,KAAK,CAAA;CACjC;AAID,OAAO,QAAQ,UAAU,CAAC;IACzB,UAAU,GAAG,CAAC;QACb,UAAU,iBAAiB;YAC1B,cAAc,EAAE,iBAAiB,CAAC,WAAW,EAAE,UAAU,GAAG,WAAW,CAAC,CAAA;SACxE;KACD;CACD"} \ No newline at end of file diff --git a/dist/jsx-types-solid.test.jsx b/dist/jsx-types-solid.test.jsx new file mode 100644 index 0000000..edf139c --- /dev/null +++ b/dist/jsx-types-solid.test.jsx @@ -0,0 +1,32 @@ +/* @jsxImportSource solid-js */ +class SomeElement extends HTMLElement { + someProp = true; + get otherProp() { + return 0; + } + set otherProp(_) { } + /** do not use this property, its only for JSX types */ + __set__otherProp; +} +SomeElement; +describe('JSX types with ElementAttributes', () => { + it('derives JSX types from classes', () => { + ; + <> + + + + {/* @ts-expect-error good, number is invalid */} + + {/* @ts-expect-error good, 'blah' is invalid */} + + + {/* Additionally TypeScript will allow unknown dash-case props (the attr: can be used here to tell Solid to set the element attributes instead of the JS properties) */} + + {/* @ts-expect-error foo doesn't exist. TypeScript will only check existence of properties without dashes */} + + ; + }); +}); +export {}; +//# sourceMappingURL=jsx-types-solid.test.jsx.map \ No newline at end of file diff --git a/dist/jsx-types-solid.test.jsx.map b/dist/jsx-types-solid.test.jsx.map new file mode 100644 index 0000000..bd4d335 --- /dev/null +++ b/dist/jsx-types-solid.test.jsx.map @@ -0,0 +1 @@ +{"version":3,"file":"jsx-types-solid.test.jsx","sourceRoot":"","sources":["../src/jsx-types-solid.test.tsx"],"names":[],"mappings":"AAAA,+BAA+B;AAI/B,MAAM,WAAY,SAAQ,WAAW;IACpC,QAAQ,GAA+B,IAAI,CAAA;IAE3C,IAAI,SAAS;QACZ,OAAO,CAAC,CAAA;IACT,CAAC;IACD,IAAI,SAAS,CAAC,CAA2B,IAAG,CAAC;IAE7C,uDAAuD;IACvD,gBAAgB,CAAiB;CACjC;AAED,WAAW,CAAA;AAUX,QAAQ,CAAC,kCAAkC,EAAE,GAAG,EAAE;IACjD,EAAE,CAAC,gCAAgC,EAAE,GAAG,EAAE;QACzC,CAAC;QAAA,EACA;GAAA,CAAC,YAAY,CAAC,SAAS,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,EAChD;GAAA,CAAC,YAAY,CAAC,SAAS,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,EAChD;GAAA,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,EAChD;GAAA,CAAC,8CAA8C,CAC/C;GAAA,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,EAC7B;GAAA,CAAC,8CAA8C,CAC/C;GAAA,CAAC,YAAY,CAAC,UAAU,CAAC,MAAM,EAE/B;;GAAA,CAAC,sKAAsK,CACvK;GAAA,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,EAC1D;GAAA,CAAC,2GAA2G,CAC5G;GAAA,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAC1B;EAAA,GAAG,CAAA;IACJ,CAAC,CAAC,CAAA;AACH,CAAC,CAAC,CAAA"} \ No newline at end of file diff --git a/dist/react.d.ts b/dist/react.d.ts index 44a4165..d87c077 100644 --- a/dist/react.d.ts +++ b/dist/react.d.ts @@ -1,14 +1,13 @@ import type { HTMLAttributes as ReactHTMLAttributes, DetailedHTMLProps as ReactDetailedHTMLProps } from 'react'; -import type { DashCasedProps } from './utils'; +import type { RemoveAccessors, RemovePrefixes, SetterTypePrefix, WithStringValues } from './LumeElement.js'; /** * Similar to ElementAttributes, but for defining element attribute types for * React JSX. See LUME Element's [TypeScript * docs](https://docs.lume.io/#/guide/making-elements?id=typescript) for * details. */ -export type ReactElementAttributes = ReactDetailedHTMLProps>>> & ReactHTMLAttributes, ElementType>; -type ToStringValues = { - [Property in keyof Type]: Type[Property] extends string ? Type[Property] : Type[Property] extends boolean ? boolean | string : string; -}; -export {}; +export type ReactElementAttributes, SetterTypePrefix>, AdditionalProperties extends object = {}> = Omit, ElementType>, SelectedProperties | keyof AdditionalProperties> & { + /** The 'has' attribute from the 'element-behaviors' package. If element-behaviors is installed and imported (it is if you're using `lume` 3D elements) then this specifies which behaviors to instantiate on the given element. */ + has?: string; +} & Partial, SetterTypePrefix>, SelectedProperties>>> & AdditionalProperties; //# sourceMappingURL=react.d.ts.map \ No newline at end of file diff --git a/dist/react.d.ts.map b/dist/react.d.ts.map index 53127c1..c33566f 100644 --- a/dist/react.d.ts.map +++ b/dist/react.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,cAAc,IAAI,mBAAmB,EAAE,iBAAiB,IAAI,sBAAsB,EAAC,MAAM,OAAO,CAAA;AAC7G,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,SAAS,CAAA;AAE3C;;;;;GAKG;AACH,MAAM,MAAM,sBAAsB,CAAC,WAAW,EAAE,kBAAkB,SAAS,MAAM,WAAW,IAAI,sBAAsB,CACrH,cAAc,CAAC,OAAO,CAAC,cAAc,CAAC,IAAI,CAAC,WAAW,EAAE,kBAAkB,CAAC,CAAC,CAAC,CAAC,GAAG,mBAAmB,CAAC,WAAW,CAAC,EACjH,WAAW,CACX,CAAA;AAED,KAAK,cAAc,CAAC,IAAI,SAAS,MAAM,IAAI;KACzC,QAAQ,IAAI,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,SAAS,MAAM,GACpD,IAAI,CAAC,QAAQ,CAAC,GACd,IAAI,CAAC,QAAQ,CAAC,SAAS,OAAO,GAC9B,OAAO,GAAG,MAAM,GAChB,MAAM;CACT,CAAA"} \ No newline at end of file +{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,cAAc,IAAI,mBAAmB,EAAE,iBAAiB,IAAI,sBAAsB,EAAC,MAAM,OAAO,CAAA;AAC7G,OAAO,KAAK,EAAC,eAAe,EAAE,cAAc,EAAE,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,kBAAkB,CAAA;AAGzG;;;;;GAKG;AACH,MAAM,MAAM,sBAAsB,CACjC,WAAW,SAAS,WAAW,EAC/B,kBAAkB,SAAS,MAAM,cAAc,CAAC,eAAe,CAAC,WAAW,CAAC,EAAE,gBAAgB,CAAC,EAC/F,oBAAoB,SAAS,MAAM,GAAG,EAAE,IACrC,IAAI,CACN,sBAAsB,CAAC,mBAAmB,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC,EACrE,kBAAkB,GAAG,MAAM,oBAAoB,CAC/C,GAEC;IACD,mOAAmO;IACnO,GAAG,CAAC,EAAE,MAAM,CAAA;CACZ,GAEC,OAAO,CAAC,gBAAgB,CAAC,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,WAAW,CAAC,EAAE,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,CAAC,CAAC,GAEnH,oBAAoB,CAAA"} \ No newline at end of file diff --git a/package.json b/package.json index 07fc684..a5f80ff 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ }, "devDependencies": { "@lume/cli": "^0.14.0", - "@types/react": "^17.0.0", + "@types/react": "npm:types-react@rc", + "@types/react-dom": "npm:types-react-dom@rc", "ncp": "^2.0.0", "prettier": "3.0.3", "typescript": "^5.0.0" @@ -44,6 +45,10 @@ "peerDependencies": { "@types/react": "*" }, + "overrides": { + "@types/react": "npm:types-react@rc", + "@types/react-dom": "npm:types-react-dom@rc" + }, "repository": { "type": "git", "url": "git+ssh://git@github.com/lume/element.git" diff --git a/src/LumeElement.ts b/src/LumeElement.ts index da2c336..7fa0013 100644 --- a/src/LumeElement.ts +++ b/src/LumeElement.ts @@ -456,13 +456,12 @@ export {LumeElement as Element} export type AttributeHandlerMap = Record -// This is TypeScript-specific. Eventually Hegel would like to have better -// support for JSX. We'd need to figure how to supports types for both systems. import type {JSX} from './jsx-runtime' 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. * @@ -503,12 +502,30 @@ type Template = TemplateContent | (() => TemplateContent) * ``` */ export type ElementAttributes< - ElementType, - SelectedProperties extends keyof ElementType, + ElementType extends HTMLElement, + SelectedProperties extends keyof RemovePrefixes, SetterTypePrefix>, AdditionalProperties extends object = {}, -> = WithStringValues>>> & - AdditionalProperties & - Omit, SelectedProperties | keyof AdditionalProperties> +> = Omit< + JSX.HTMLAttributes, + 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 + } + + & Partial< + DashCasedProps< + WithStringValues< + Pick< + RemovePrefixes, SetterTypePrefix>, + SelectedProperties + > + > + > + > + + & AdditionalProperties /** * Make all non-string properties union with |string because they can all @@ -516,6 +533,34 @@ export type ElementAttributes< * are converted to the types of values they should be, f.e. reading a * `@numberAttribute` property always returns a `number`) */ -type WithStringValues = { - [Property in keyof Type]: NonNullable extends string ? Type[Property] : Type[Property] | string +export type WithStringValues = { + // [Property in keyof Type]: NonNullable extends string ? Type[Property] : Type[Property] | string + [Property in keyof Type]: PickFromUnion extends never + ? // if the type does not include a type assignable to string + Type[Property] | string + : // otherwise it does + Type[Property] +} + +type StringKeysOnly = OmitFromUnion + +type OmitFromUnion = T extends TypeToOmit ? never : T +type PickFromUnion = T extends TypeToPick ? T : never + +export type RemovePrefixes = { + [K in keyof T as K extends string ? RemovePrefix : K]: T[K] } + +type RemovePrefix = T extends `${Prefix}${infer Rest}` ? Rest : T + +export type RemoveAccessors = { + [K in keyof T as K extends RemovePrefix>, SetterTypePrefix> ? never : K]: T[K] +} + +type SetterTypeKeysFor = keyof PrefixPick + +type PrefixPick = { + [K in keyof T as K extends `${Prefix}${string}` ? K : never]: T[K] +} + +export type SetterTypePrefix = '__set__' diff --git a/src/jsx-types-react.test.tsx b/src/jsx-types-react.test.tsx new file mode 100644 index 0000000..d8bc68a --- /dev/null +++ b/src/jsx-types-react.test.tsx @@ -0,0 +1,44 @@ +/* @jsxImportSource react */ + +import type {ReactElementAttributes} from './react.js' + +class SomeElement extends HTMLElement { + someProp: 'true' | 'false' | boolean = true + + get otherProp(): number { + return 0 + } + set otherProp(_: this['__set__otherProp']) {} + + /** do not use this property, its only for JSX types */ + __set__otherProp!: number | 'foo' +} + +SomeElement + +declare module 'react' { + namespace JSX { + interface IntrinsicElements { + 'some-element': ReactElementAttributes + } + } +} + +describe('JSX types with ReactElementAttributes', () => { + it('derives JSX types from classes', () => { + ;<> + + + + {/* @ts-expect-error good, number is invalid */} + + {/* @ts-expect-error good, 'blah' is invalid */} + + + {/* Additionally TypeScript will allow unknown dash-case props (as we didn't not define JS properties with these exact dash-cased names, React 19+ will set the element attributes, useful for setting the attributes but React has no way to specify to set attributes for names without dashes) */} + + {/* @ts-expect-error foo doesn't exist. TypeScript will only check existence of properties without dashes */} + + + }) +}) diff --git a/src/jsx-types-solid.test.tsx b/src/jsx-types-solid.test.tsx new file mode 100644 index 0000000..913556f --- /dev/null +++ b/src/jsx-types-solid.test.tsx @@ -0,0 +1,44 @@ +/* @jsxImportSource solid-js */ + +import type {ElementAttributes} from './LumeElement.js' + +class SomeElement extends HTMLElement { + someProp: 'true' | 'false' | boolean = true + + get otherProp(): number { + return 0 + } + set otherProp(_: this['__set__otherProp']) {} + + /** do not use this property, its only for JSX types */ + __set__otherProp!: number | 'foo' +} + +SomeElement + +declare module 'solid-js' { + namespace JSX { + interface IntrinsicElements { + 'some-element': ElementAttributes + } + } +} + +describe('JSX types with ElementAttributes', () => { + it('derives JSX types from classes', () => { + ;<> + + + + {/* @ts-expect-error good, number is invalid */} + + {/* @ts-expect-error good, 'blah' is invalid */} + + + {/* Additionally TypeScript will allow unknown dash-case props (the attr: can be used here to tell Solid to set the element attributes instead of the JS properties) */} + + {/* @ts-expect-error foo doesn't exist. TypeScript will only check existence of properties without dashes */} + + + }) +}) diff --git a/src/react.ts b/src/react.ts index 3b9ca92..11e59d0 100644 --- a/src/react.ts +++ b/src/react.ts @@ -1,21 +1,27 @@ import type {HTMLAttributes as ReactHTMLAttributes, DetailedHTMLProps as ReactDetailedHTMLProps} from 'react' -import type {DashCasedProps} from './utils' +import type {RemoveAccessors, RemovePrefixes, SetterTypePrefix, WithStringValues} from './LumeElement.js' +// prettier-ignore /** * Similar to ElementAttributes, but for defining element attribute types for * React JSX. See LUME Element's [TypeScript * docs](https://docs.lume.io/#/guide/making-elements?id=typescript) for * details. */ -export type ReactElementAttributes = ReactDetailedHTMLProps< - DashCasedProps>>> & ReactHTMLAttributes, - ElementType -> +export type ReactElementAttributes< + ElementType extends HTMLElement, + SelectedProperties extends keyof RemovePrefixes, SetterTypePrefix>, + AdditionalProperties extends object = {}, +> = Omit< + ReactDetailedHTMLProps, ElementType>, + SelectedProperties | keyof AdditionalProperties + > -type ToStringValues = { - [Property in keyof Type]: Type[Property] extends string - ? Type[Property] - : Type[Property] extends boolean - ? boolean | string - : string -} + & { + /** The 'has' attribute from the 'element-behaviors' package. If element-behaviors is installed and imported (it is if you're using `lume` 3D elements) then this specifies which behaviors to instantiate on the given element. */ + has?: string + } + + & Partial, SetterTypePrefix>, SelectedProperties>>> + + & AdditionalProperties