Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How do I use this with TypeScript? #19

Open
balupton opened this issue May 5, 2019 · 7 comments
Open

How do I use this with TypeScript? #19

balupton opened this issue May 5, 2019 · 7 comments

Comments

@balupton
Copy link

balupton commented May 5, 2019

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)

@ithinkihaveacat
Copy link
Contributor

ithinkihaveacat commented Jul 24, 2019

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.

This was referenced Jun 16, 2020
@ThaNarie
Copy link

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
Copy link

@types/vhtml is now public (see #25 (comment)). I think we can close this issue.

@AndrewLeedham
Copy link

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
Copy link

@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.

@pastelmind
Copy link

pastelmind commented 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.

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:

  1. noChild is given props.children === undefined
  2. oneChild is given props.children === <div>the child</div>
  3. manyChildren is given props.children === [ <div>child1</div>, <div>child2</div>, <div>child3</div> ]

However, vhtml actually does this:

  1. noChild is given props.children === []
  2. oneChild is given props.children === [ <div>the child</div> ]
  3. 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:

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.

  1. Change vhtml to handle children like React does. Will be a semver-major change, and move away from Preact-like semantics.
  2. Ask TypeScript to handle JSX differently. Unlikely to work since the status quo works just fine for React.
  3. 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.

pastelmind added a commit to pastelmind/vhtml-types that referenced this issue Jan 7, 2021
This reverts commit 19aa2a9.

This won't work, because vhtml handles the `props.children` attribute in
a way different from how TypeScript expects things.

TypeScript believes that:

- A component with no child receives props.children === undefined
- A component with one child receives props.children === typeof child
- A component with multiple children receives [...children]

However, vhtml always wraps children in an array, even if there is only
0 or 1 child.

Because of this, enforcing strict type checking on children would result
in incorrect type checks that could be actively harmful.

See more discussion and example at:

- developit/vhtml#19 (comment)
@pastelmind
Copy link

pastelmind commented 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:

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

pastelmind added a commit to pastelmind/vhtml-types that referenced this issue Mar 18, 2021
Instead of injecting the `JSX` namespace as a global symbol, move it
under a new namespace named `vhtml`.
This `vhtml` namespace is merged with the `vhtml()` function. Library
consumers can access it as `.JSX`:

```ts
import h from "vhtml";
type A = h.JSX.Element;
```

The `JSX` namespace can also be imported directly:

```
import h, {JSX} from "vhtml";
type A = JSX.Element;
```

This should allow vhtml-powered JSX code to be used together with other
JSX-based libraries, such as React. It _should_ solve the issue:

- developit/vhtml#19 (comment)

Background: TypeScript 2.8 supports locally scoping the `JSX` namespace.
This allows a package to use multiple libraries that expose different
variations of the `JSX` namespace without conflicts.

See:
- https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#locally-scoped-jsx-namespaces
- microsoft/TypeScript#22207

Note: This does not allow you to use two JSX-based libraries together
in a single module. No transpiler that I know of supports this use case.
pastelmind added a commit to pastelmind/vhtml-types that referenced this issue Mar 18, 2021
Instead of injecting the `JSX` namespace as a global symbol, move it
under a new namespace named `vhtml`.
This `vhtml` namespace is merged with the `vhtml()` function. Library
consumers can access it as `.JSX`:

    import h from "vhtml";
    type A = h.JSX.Element;

The `JSX` namespace can also be imported directly:

    import h, {JSX} from "vhtml";
    type A = JSX.Element;

This should allow vhtml-powered JSX code to be used together with other
JSX-based libraries, such as React. It _should_ solve the issue:

- developit/vhtml#19 (comment)

Background: TypeScript 2.8 supports locally scoping the `JSX` namespace.
This allows a package to use multiple libraries that expose different
variations of the `JSX` namespace without conflicts.

See:
- https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#locally-scoped-jsx-namespaces
- microsoft/TypeScript#22207

Note: This does not allow you to use two JSX-based libraries together
in a single module. No transpiler that I know of supports this use case.
typescript-bot pushed a commit to DefinitelyTyped/DefinitelyTyped that referenced this issue Apr 4, 2021
@pastelmind

BREAKING CHANGE:
Instead of injecting the `JSX` namespace as a global symbol, move it
under a new namespace named `vhtml`.
This `vhtml` namespace is merged with the `vhtml()` function. Library
consumers can access it as `.JSX`:

    import h from "vhtml";
    type A = h.JSX.Element;

The `JSX` namespace can also be imported directly:

    import h, {JSX} from "vhtml";
    type A = JSX.Element;

This should allow vhtml-powered JSX code to be used together with other
JSX-based libraries, such as React. It _should_ solve the issue:

- developit/vhtml#19 (comment)

Background: TypeScript 2.8 supports locally scoping the `JSX` namespace.
This allows a package to use multiple libraries that expose different
variations of the `JSX` namespace without conflicts.

See:
- https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#locally-scoped-jsx-namespaces
- microsoft/TypeScript#22207

Note: This does not allow you to use two JSX-based libraries together
in a single module. No transpiler that I know of supports this use case.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants