How do I use this with TypeScript? · Issue #19 · developit/vhtml · GitHub
Skip to content

How do I use this with TypeScript? #19

Open

Description

@balupton

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)

Activity

  1. ithinkihaveacat commented on Jul 24, 2019

    @ithinkihaveacat
    Contributor

    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.

  2. ThaNarie commented on Nov 27, 2020

    @ThaNarie

    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 :)

  3. pastelmind commented on Dec 24, 2020

    @pastelmind

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

  4. AndrewLeedham commented on Dec 24, 2020

    @AndrewLeedham

    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

  5. pastelmind commented on Dec 25, 2020

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

  6. pastelmind commented on Jan 7, 2021

    @pastelmind

    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.

  7. pastelmind commented on Jan 11, 2021

    @pastelmind

    @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
  8. added a commit that references this issue on Apr 4, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions