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