Skip to content

Type JSX elements based on createElement function #14729

Open

Description

TypeScript currently uses a JSX namespace to type the props of JSX elements. This is customisable by modifying JSX.IntrinsicElements, JSX.ElementClass. However, the type of an element is always JSX.Element.

Of course more interfaces could be added to the JSX namespace. However, I think that with less effort, the compiler could be made more flexible. I'd propose to check JSX elements based on a (virtual) call to JSX.createElement. For instance, the current behaviour can approximately be written like this:

namespace JSX {
  // intrinsic elements
  function createElement<K extends keyof JSX.IntrinsicElements>(tag: K, props: JSX.IntrinsicElements[K], children: JSX.Element[]): JSX.Element;
  // class and functional components
  function createElement<P>(component: (JSX.ElementClass & { new(): { props: P } }) | ((props: P) => JSX.Element), props: P, children: JSX.Element[]): JSX.Element;
}

Given that function signatures are well customisable with the use of generics for instance, most requests can be implemented this way. For instance, #13890 and #13618 would benefit from this change. Libraries that use JSX, but not on the React-way, will benefit from this too.

Proposed semantics

For a JSX element, a virtual call to JSX.createElement is constructed. It is passed three arguments:

  1. Tag name (string) in case of an intrinsic argument (<div />). Otherwise, the identifier is passed (Foo for <Foo />).
  2. An object containing the props.
  3. An array containing the children.

This should be roughly the same as the JSX transform used in the emitter phase. One notable difference is the following: in case of no properties or no children, an empty object or an empty array should be passed, instead ignoring the argument. This makes it easier to write JSX.createElement for library authors.

Backwards compatibility

For backwards compatibility, one of the following approaches could be taken:

  • Use old logic if JSX.createElement does not exist.
  • If JSX.createElement does not exist, default it to (roughly) the definition above.

Error reporting

When type checking the generated function call, the checker can give error messages like "Argument of type .. " or "Supplied parameters do not match any signature of call target". These messages should be replaced when checking JSX elements.

Activity

  1. RyanCavanaugh commented on Mar 20, 2017

    @RyanCavanaugh
    Member

    I think this is doable and could potentially cut out a lot of code from the compiler. When we added JSX support we didn't have spread types or keyof / T[k] so this was impossible. Behavior-wise I think this approach would allow all current behavior to be represented with the exception of data- and aria-, which would could special case to not present excess property errors.

    The remaining difficulty is just technical - the overload resolution code is hardcoded in a lot of places with the assumption that it's operating over an actual argument list syntax tree rather than some abstract set of parameters.

  2. niieani commented on Mar 22, 2017

    @niieani

    Having this would allow one to utilize the concept of generalized JSX with TS. Currently it seems impossible, because the return type of any JSX expression is always a non-generic JSX.Element.

    In fact, given this it would make sense to ditch the whole "JSX" global namespace and just use the declared type of the locally scoped jsxFactory (set in the tsconfig.json), so that the return type of a JSX expression could be different in every file, based on the implementation of the jsxFactory.

  3. ivogabe commented on Apr 4, 2017

    @ivogabe
    ContributorAuthor

    I think this is doable and could potentially cut out a lot of code from the compiler. When we added JSX support we didn't have spread types or keyof / T[k] so this was impossible.

    That's exactly what I thought!

    The remaining difficulty is just technical - the overload resolution code is hardcoded in a lot of places with the assumption that it's operating over an actual argument list syntax tree rather than some abstract set of parameters.

    The compiler already handles different kinds of function calls. resolveSignature in checker.ts works with normal call expressions, but also tagged templates and decorators. Those last two don't have an explicit argument list in the source code, so I think that JSX components could be integrated there too? Actually, that function already handles JSX elements for the language service, when used with SFCs.

    Bazyli Brzóska (@niieani) That's an interesting use case. I think that it would be useful in a lot of situations, other than the DOM, where some domain specific language is desired.

  4. niieani commented on Apr 6, 2017

    @niieani

    Ivo Gabe de Wolff (@ivogabe) yeah, I especially like the example with ASTs, represented by JSX. So much more readable than declaring plain objects for AST.

  5. ceymard commented on May 15, 2017

    @ceymard

    I'd like to ping this issue ; could it be possible that the whole jsx mechanic be simply a transform to a function call with its implied checking ?

    This would allow for so much flexibility in library creation.

  6. RyanCavanaugh commented on May 15, 2017

    @RyanCavanaugh
    Member

    could it be possible that the whole jsx mechanic be simply a transform to a function call with its implied checking ?

    No. Two reasons and there may be more:

    • In React properties like data-foo and aria-bar are special, but we'd incorrectly flag these as errors
    • We wouldn't detect excess properties if there was a spread [1]

    [1]

    interface Point {
        x: number;
        y: number;
    }
    
    var m: Point;
    var j: Point = { z: 10, ...m }; // Not an error due to spread
  7. niieani commented on May 16, 2017

    @niieani

    Ryan Cavanaugh (@RyanCavanaugh) The first could be fixed via other types of constructs that could be added to the language:

    WithDomProperties<Point> -- adds typing data-* and aria-* to the type

    Second we could think about. Maybe the spreads in JSX would behave differently?

    There's probably something we could do. In any case, this would massively simplify the API and allow for custom JSX applications as I've mentioned in the comment above.

  8. amir-arad commented on Jun 29, 2017

    @amir-arad

    AFAIK it's not just data-* and area-*, it's *-*.

    class Foo extends React.Component<{a:string}>{}
    
    //This works:
    <Foo b-3="bar" a="ggg"/>
  9. joewood commented on Dec 12, 2017

    @joewood

    Ryan Cavanaugh (@RyanCavanaugh) now that the second point has been fixed, can this be revisited?

    interface Point {
        x: number;
        y: number;
    }
    
    var m: Point;
    var j: Point = { z: 10, ...m }; // NOW ERRORS
  10. jchitel commented on Feb 6, 2018

    @jchitel

    Another problem to consider here is how to deal with backwards compatibility, particularly around the DefinitelyTyped definitions for React, which would have to be updated to support this.

    Existing types that depend on the JSX namespace would no longer work because the compiler wouldn't look there anymore. The most graceful way I can see to handle that would be to modify the React definitions so that the JSX namespace definitions are dependent on (or at least structurally equivalent to) whatever types are used for React.createElement, and deprecate the JSX namespace.

    For non-React use cases, this will probably just have to be a breaking change, with a suggested migration pattern. If someone is already using the JSX namespace for typing JSX, all they need to do is add a jsxFactory function (which they probably already have) and tie their JSX types to it.

    I agree that having this would improve the flexibility of JSX-in-TS considerably, but the migration for existing code will probably be quite painful.

  11. mhegazy commented on Feb 13, 2018

    @mhegazy
    Contributor

    should be covered by #21699

  12. 40 remaining items

  13. TylorS commented on Mar 2, 2023

    @TylorS

    I've been following various JSX threads over the years all boiling down to this, I really truly hope this lands in TS at some point even if not 5.1 directly.

    As you fear the performance of JSX return types, what about a compromise? Either:

    • enable with an option that defaults to disabled?
    • allow return types only for top level result of a JSX expression so at least something like const div =
      ...
      regardless of what children the div has.

    In response to this question, I sincerely hope for it to be 1. The type-safety of these functions seems most important over performance issues. The Effect community (formerly fp-ts) and I are building libraries today for html rendering and are today forced to use tagged template literals to be able to track typed resources and typed errors of children to be able to aggregate the resources required to run them and the errors they might produce as unions.

    If JSX uses createElement as any other function for type-checking, we'd be able to utilize JSX's AST to further optimize them using compiler techniques and provide much more type-safe APIs for properties which is a lot more cumbersome to do with template literal strings and requires language service/server plugins to work as expected.

  14. TylorS commented on Apr 14, 2023

    @TylorS

    Ryan Cavanaugh (@RyanCavanaugh) You closed this with #51328, but it doesn't at all lookup the types by the signature of createElement. I think this should remain open

  15. yairopro commented on Jun 14, 2023

    @yairopro

    I just add here a pure sample code of the problem:

    https://codesandbox.io/s/jsx-element-loses-types-6nmh3m?file=/src/App.tsx

  16. arthurfiorette commented on Sep 18, 2023

    @arthurfiorette

    This was planned for milestone 5.1. Any updates here?

  17. frzi commented on Apr 25, 2024

    @frzi

    Is there still any interest or plans for this? This seems like an incredible feature that’ll make libraries utilizing JSX far more versatile. 😄

  18. robbiespeed commented on Apr 26, 2024

    @robbiespeed

    Here's a couple TS Playground examples of how we could use this feature to enable similar features to flow's "renders" types and limiting allowed children or other props that receive jsx:

  19. WorldMaker commented on Oct 3, 2025

    @WorldMaker

    I've been thinking about this some this week and hitting some of the hard limits of the current namespace-based approach to JSX types. I've been exploring an architecture where different types of JSX functions get "dependency-injected" depending on runtime need or downstream usage of the JSX. Runtime support for it works just fine (using the original "jsx": "react", "jsxFactory": "jsx" where you have to make sure your jsx function is in scope yourself), but I'd also love to get the types as rich as possible. At the moment I am struggling to get any types working via the ambient namespace tooling, and ideally it would be nice if the JSX types could be themselves captured in a way that is "dependency-injectable" (a part of the type of the jsx function itself.

    Say that I have an interface:

    import { jsx } from './jsx.js'
    
    export interface Context<T>
    {
      // …other parts of the context
      jsx: typeof jsx
    }

    Even if that jsx import is well-defined and included the ambient namespaces specific to it when imported directly, the ambient namespace isn't considered part of the typeof and attempting to use this jsx function results in type errors:

    import { Context } from './context.js'
    import { run, RunContext } from './run.js'
    
    run((_props: object, { jsx }: Context<RunContext> /* maybe inferable from run's type */) => <h1>Test Example</h1>)
    // => JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists.

    I think I can set a single global JSX namespace (though that doesn't seem to be working right now for me either when I try the obvious things; ambient namespaces seem so far behind the times now on type import/export), but it would be nice to maybe also vary Element based on the generic parameter to the "Context" object (or something to that effect) or even swap IntrinsicElements (such as for a specialized context for SVG instead of HTML, for instance).

    It's possible my intended architecture is just beyond the scope of intended JSX usage in Typescript, but I thought I'd bring it up if it sparked further ideas on improving how JSX is typed in general in Typescript, and what this issue suggests of trying to get more of the type information directly on the createElement function sounds close to a path forward that I could use.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

In DiscussionNot yet reached consensusSuggestionAn idea for TypeScript

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions