You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Although the above works - if any of your code imports something that imports React (like storybook), those global JSX types will be included, and those will be prioritised over [elemName: string]: any;.
And anything that's imported explicitly (even through something else) cannot be ignored/excluded in the tsconfig.json :(
The same issue @ThaNarie mentioned about global JSX occurs in the DefinitelyTyped version as well right? Perhaps an issue for that can be opened there for something like the preact types approach.
Either way I think this is resolved. Thanks @pastelmind
@AndrewLeedham Probably. I suppose we could split the JSX.IntrinsicElements interface into a separate jsx.d.ts file. Then users who want vhtml JSX semantics would need to add this to the top of their code:
/// <reference types="vhtml/jsx" />// This can also workimport{}from"vhtml/jsx";
When TypeScript examines the props of a function component, it assumes:
noChild is given props.children === undefined
oneChild is given props.children === <div>the child</div>
manyChildren is given props.children === [ <div>child1</div>, <div>child2</div>, <div>child3</div> ]
However, vhtml actually does this:
noChild is given props.children === []
oneChild is given props.children === [ <div>the child</div> ]
manyChildren is given props.children === [ <div>child1</div>, <div>child2</div>, <div>child3</div> ]
Only the last assumptions match.
Because of this, TypeScript will happily accept the following code:
constWantString=(props: {children: string})=>{// Should be fine, right?returnprops.children.toLowerCase();}// TypeScript: I think you're receiving props.children === "asdf"// vhtml: Nope, I will pass props.children === ["asdf"] and your component will die trying to lowercase an arrayconstresult=<WantString>{"asdf"}</WantString>
The only way of guaranteeing type safety is to type all children as any, which defeats the purpose of type checking in the first place.
I suspect that TypeScript's assumptions are largely based on how React.createElement() works. There are several solutions for this.
Change vhtml to handle children like React does. Will be a semver-major change, and move away from Preact-like semantics.
Ask TypeScript to handle JSX differently. Unlikely to work since the status quo works just fine for React.
Write a new library (possibly in TypeScript) similar to vhtml, but better conformance to TypeScript.
Edit: Looks like Preact has got into this as well. See preactjs/preact#1008 and preactjs/preact#1116 where they hacked their way around the discrepancy between Preact's children handling behavior and TypeScript's assumptions.
@types/vhtml 2.2.1 supports strict children type checks!
preactjs/preact#1116 (comment) gave me a hint to look into JSX.LibraryManagedAttributes, an obscure feature implemented in TypeScript 3.0. I used it to transform function component types into shapes that TypeScript recognizes for JSX type checking.
If you use a tuple literal with exactly one element, TypeScript will check that too.
// vhtml will flatten and concatenate arrays in JSX, so this is fineconstOneChild(props: {children: [string]})=><div>{props.children}</div>;letresult1=<OneChild/>;// Compile errorletresult2=<OneChild>Yes</OneChild>;// OKletresult3=<OneChild><div>1</div><div>2</div></OneChild>;// Compile error
Rejecting invalid children type
vhtml always passes props.children as an array. TypeScript can enforce this, too:
// props.children is not an array!constBadComponent(props: {children: string})=><div>{props.children}</div>;letresult1=<BadComponent/>;// Compile errorletresult2=<BadComponent>Yes</BadComponent>;// Compile errorletresult3=<BadComponent><div>1</div><div>2</div></BadComponent>;// Compile error
All this is made possible by using a series of conditional checks. Should one need to support more type checks, all we need is to expand those checks.
Limitations
Unfortunately, these tricks aren't as flexible as I would like them to be. For example, you can't enforce type checks for N-length tuple types (N > 1)--TypeScript will simply treat them as arbitrary-length arrays
// I want exactly three children, in the order of boolean, string, numberconstImperfect(props: {children: [boolean,string,number];})=>{/* ... */};// ...but TypeScript will treat the above as (boolean | string | number)[]// so this compiles just fine:<Imperfect>{true}</Imperfect>;
I haven't bothered to tackle this, but it seems we would need variadic tuples (introduced in TypeScript 4.0) to make it happen. Since TypeScript 3.x is still going strong, it would be prudent not to use bleeding edge features in our type definitions.
(If you can make it happen in TypeScript 3.x, please submit a PR to DefinitelyTyped! We would appreciate it very much.)
Extending intrinsic attributes
Another caveat: To ensure that plain HTML tags accept anything as children, they internally have a children: any property. However, this is incompatible with function component prop types, which require that children: any[]. This can become a problem when you want to extend the attributes of built-in tags:
// I want my component to act like a <div> with benefits.// I know: I'll extend the built-in interface used for type checking <div>typeProps=JSX.IntrinsicAttributes['div']&{someProp?: number};constBetterDiv=(props: Props)=>{const{ children, ...rest}=props;return<div{...rest}>{/* ... */}</div>;}<BetterDivsomeProp={12}>foobar</BetterDiv>;//Compileerror
To work around the issue, always define your own children prop:
Activity
ithinkihaveacat commentedon Jul 24, 2019
I think you're after a declaration like this:
See https://www.typescriptlang.org/docs/handbook/jsx.html#intrinsic-elements.
ThaNarie commentedon Nov 27, 2020
Although the above works - if any of your code imports something that imports React (like storybook), those global JSX types will be included, and those will be prioritised over
[elemName: string]: any;.And anything that's imported explicitly (even through something else) cannot be ignored/excluded in the
tsconfig.json:(Hoping someone else has a solution for this :)
pastelmind commentedon Dec 24, 2020
@types/vhtml is now public (see #25 (comment)). I think we can close this issue.
AndrewLeedham commentedon Dec 24, 2020
The same issue @ThaNarie mentioned about global JSX occurs in the DefinitelyTyped version as well right? Perhaps an issue for that can be opened there for something like the preact types approach.
Either way I think this is resolved. Thanks @pastelmind
pastelmind commentedon Dec 25, 2020
@AndrewLeedham Probably. I suppose we could split the
JSX.IntrinsicElementsinterface into a separatejsx.d.tsfile. Then users who want vhtml JSX semantics would need to add this to the top of their code:@types/react seems to be using similar approach. See
experimental.d.tsSince I am unfamiliar with this trick, I am hesitant to work on it myself. Feel free to submit a PR to DefinitelyTyped.
pastelmind commentedon Jan 7, 2021
I've been experimenting with enabling children type-checking with TypeScript. I was unsuccessful.
It turns out TypeScript has some strict assumptions about how child components are passed around in JSX.
When TypeScript examines the
propsof a function component, it assumes:noChildis givenprops.children === undefinedoneChildis givenprops.children === <div>the child</div>manyChildrenis givenprops.children === [ <div>child1</div>, <div>child2</div>, <div>child3</div> ]However, vhtml actually does this:
noChildis givenprops.children === []oneChildis givenprops.children === [ <div>the child</div> ]manyChildrenis givenprops.children === [ <div>child1</div>, <div>child2</div>, <div>child3</div> ]Only the last assumptions match.
Because of this, TypeScript will happily accept the following code:
The only way of guaranteeing type safety is to type all children as
any, which defeats the purpose of type checking in the first place.I suspect that TypeScript's assumptions are largely based on how
React.createElement()works. There are several solutions for this.childrenlike React does. Will be a semver-major change, and move away from Preact-like semantics.Edit: Looks like Preact has got into this as well. See preactjs/preact#1008 and preactjs/preact#1116 where they hacked their way around the discrepancy between Preact's
childrenhandling behavior and TypeScript's assumptions.Revert "feat: Enable type checking of component children"
pastelmind commentedon Jan 11, 2021
@types/vhtml 2.2.1 supports strict children type checks!
preactjs/preact#1116 (comment) gave me a hint to look into
JSX.LibraryManagedAttributes, an obscure feature implemented in TypeScript 3.0. I used it to transform function component types into shapes that TypeScript recognizes for JSX type checking.Examples
For example, given the following component:
TypeScript will allow any number of children, as long as they all evaluate to a string:
Childless component
If you omit
props.children, TypeScript will interpret it as a "childless component":Single-child component
If you use a tuple literal with exactly one element, TypeScript will check that too.
Rejecting invalid children type
vhtml always passes
props.childrenas an array. TypeScript can enforce this, too:All this is made possible by using a series of conditional checks. Should one need to support more type checks, all we need is to expand those checks.
Limitations
Unfortunately, these tricks aren't as flexible as I would like them to be. For example, you can't enforce type checks for N-length tuple types (N > 1)--TypeScript will simply treat them as arbitrary-length arrays
I haven't bothered to tackle this, but it seems we would need variadic tuples (introduced in TypeScript 4.0) to make it happen. Since TypeScript 3.x is still going strong, it would be prudent not to use bleeding edge features in our type definitions.
(If you can make it happen in TypeScript 3.x, please submit a PR to DefinitelyTyped! We would appreciate it very much.)
Extending intrinsic attributes
Another caveat: To ensure that plain HTML tags accept anything as children, they internally have a
children: anyproperty. However, this is incompatible with function component prop types, which require thatchildren: any[]. This can become a problem when you want to extend the attributes of built-in tags:To work around the issue, always define your own children prop:
feat!: Move JSX from global namespace to vhtml.JSX
feat!: Move JSX from global namespace to vhtml.JSX
🤖 Merge PR #51834 vhtml: Move JSX from global namespace to vhtml.JSX by