How do I use this with TypeScript?
I can figure out the:
{
"compilerOptions": {
"jsx": "react",
"jsxFactory": "h",
},
}
But I can't figure out how to get the typings going for h and things like children,
Specifically the error I'm getting on any JSX element is:
JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists.ts(7026)
I think you're after a declaration like this:
declare namespace JSX {
interface IntrinsicElements {
[elemName: string]: any;
}
}
See https://www.typescriptlang.org/docs/handbook/jsx.html#intrinsic-elements.
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 :)
@types/vhtml is now public (see https://github.com/developit/vhtml/issues/25#issuecomment-750644187). I think we can close this issue.
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 work
import {} from "vhtml/jsx";
@types/react seems to be using similar approach. See experimental.d.ts
Since I am unfamiliar with this trick, I am hesitant to work on it myself. Feel free to submit a PR to DefinitelyTyped.
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.
function MyComponent(props: { children: any }): JSX.Element {
/* ... */
}
const noChild = <MyComponent/>;
const oneChild = <MyComponent><div>the child</div></MyComponent>;
const manyChildren = (
<MyComponent>
<div>child 1</div>
<div>child 2</div>
<div>child 3</div>
</MyComponent>
);
When TypeScript examines the props of a function component, it assumes:
-
noChildis givenprops.children === undefined -
oneChildis 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:
const WantString = (props: { children: string }) => {
// Should be fine, right?
return props.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 array
const result = <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
childrenlike 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 https://github.com/preactjs/preact/issues/1008 and https://github.com/preactjs/preact/pull/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!
https://github.com/preactjs/preact/pull/1116#issuecomment-414004721 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:
function Component(props: { children: string[] }): JSX.Element {
/* ... */;
}
TypeScript will allow any number of children, as long as they all evaluate to a string:
<Component>Foo{"bar"}<div>baz</div></Component>; // OK
<Component>{1}</Component>; // Error!
Childless component
If you omit props.children, TypeScript will interpret it as a "childless component":
const NoChildren() => <div>No children!</div>;
let result1 = <NoChildren/>; // OK
let result2 = <NoChildren>This won't work</NoChildren>; // Compile error
Single-child component
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 fine
const OneChild(props: { children: [string] }) => <div>{props.children}</div>;
let result1 = <OneChild/>; // Compile error
let result2 = <OneChild>Yes</OneChild>; // OK
let result3 = <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!
const BadComponent(props: { children: string }) => <div>{props.children}</div>;
let result1 = <BadComponent/>; // Compile error
let result2 = <BadComponent>Yes</BadComponent>; // Compile error
let result3 = <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, number
const Imperfect(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>
type Props = JSX.IntrinsicAttributes['div'] & { someProp?: number };
const BetterDiv = (props: Props) => {
const { children, ...rest } = props;
return <div {...rest}>{ /* ... */ }</div>;
}
<BetterDiv someProp={12}>foo bar</BetterDiv>; // Compile error
To work around the issue, always define your own children prop:
type DivProps = JSX.IntrinsicAttributes['div'];
interface Props extends DivProps {
children: any[];
someProp?: number;
}
const BetterDiv = (props: Props) => {
const { children, ...rest } = props;
return <div {...rest}>{ /* ... */ }</div>;
}
<BetterDiv someProp={12}>foo bar</BetterDiv>; // OK