-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Decouple jsx element type from jsx factory return type and sfc return type #21699
Comments
@weswigham would addressing this address #18357 ? It'd be great to be able to correctly type |
There are a lot of issues related to this that have been closed, saying that this issue will handle them. I am assuming that the case in #18357 will be handled by this as well. At the core of this is the ability to type JSX based off of the |
@jchitel agreed. I just hit this snag trying to change the react typings yesterday for this exact same reason. |
@weswigham If you haven't started on this yet, I'd like to take a crack at it |
@ericanderson I actually started work on this today, sorry 😉 |
@weswigham Damn. Can you make sure I can capture the SFC or ComponentClass so we can do things like limit the children of |
@ericanderson That's the plan. (I mean, technically it was a bug report a long time ago that we closed as "fixed" even though it was only half-fixed) |
I think that will also let us mark the defaultProps optional. |
By |
I mean: interface Props {
foo: string,
bar: number
}
class Foo extends React.Component<Props> {
static defaultProps = { foo: "Hi Mom!" };
//...
} In this world, the compiler should only require me to specify |
... maybe. Even with localized types looked up from the signatuers, we still have to make assumptions about how the custom ctor/sfc is effectively built, because there's actually two calls going on. The first call is the constructor for the class or the SFC, (which are expected to take props as an argument), and can be overloaded - this is where a lot of props validation actually happens. The second call is the factory function itself, which actually needs the reified types from the inner call to be typechecked correctly (And today isn't actually typechecked at all, which is the root of most of these issues). TBQH a lot of the JSX machinery in-place today is to get higher-ordery behavior from a system that used to not support any. I think now that we have conditional and infer types, there's a good chance I may actually be able to desugar a lot of the magic currently applied to JSX to normal typesystem operations. I'm generally just going to look at improving this as much as is feasible 😉 |
So the props we should enforce are actually |
@weswigham Thanks! |
I'm not entirely sure that this was technically fixed. #18131 was fixed, but we still don't actually look at what a given JSX factory invocation returns. |
Which would still work if this issue were resolved and the Typescript compiler would respect the function jsxFactory<T extends keyof HTMLElementTagNameMap>(
type: T,
props: Partial<HTMLElementTagNameMap[T]>,
...children: HTMLElement[]
): HTMLElementTagNameMap[T] (A very crude example 😄 ) I agree with others that the current JSX implementation is rather... unnecessarily complex. Having all these specific 'ghost' JSX interfaces you can define to |
@bluepnume SolidJS doesn't compile JSX to simple function calls. They currently compile them to IIFEs for the DOM, and a |
Sure, but I don't think it's that outlandish for typescript to default to simple function calls given that that is already how the /** @jsx jsxFactory */
const foo = ( <div /> ) "use strict";
/** @jsx jsxFactory */
const foo = (jsxFactory("div", null)); -- it feels like this should be the default for the type system too, with some way to override for frameworks like SolidJS that don't follow this convention? |
Yes, that's called a syntactic sugar and I think that's the most future proof way to do it in my opinion but that may not be the most practical one considering the kind of baggage TS carries due to backward compatibility in terms of both functionality and performance. If a target library (like SolidJS) needs to use a black box, they are free to do so. |
@bluepnume TS also supports outputting JSX literally, mainly for React Native, but Solid requires this as well. |
Is this the issue that tracks the ability to make this finally happen? declare namespace JSX {
type IntrinsicElementTypes = {
button: HTMLButtonElement,
a: HTMLAnchorElement,
ul: HTMLUListElement,
'': DocumentFragment,
}
}
const b = <button />; // now inferred as HTMLButtonElement
const a = <a />; // now inferred as HTMLAnchorElement
const f = <Foo />; // now inferred as HTMLUListElement
const d = <></>; // now inferred as DocumentFragment
function Foo() /* return type inferred as HTMLUListElement */ {
return <Bar />;
}
function Bar() /* return type inferred as HTMLUListElement */ {
return <ul />;
} Because I really need that feature since I use vanillajsx daily. I'd be glad to contribute towards this feature full time until it's done. Also, type IntrinsicElementTypes = {
[tag: string]: JSX.Element,
'': JSX.Fragment,
} |
This should be pinned to the top of this thread to prevent others from wasting their time here. |
Assuming @reverofevil is correct. He's certainly confident, but I've met too many overconfident software engineers who turned out to be wrong. I'd need to see some kind of proof, or someone with reknowned credibility confirming this feature is "unfixable". |
@sdegutis I came to this issue for a very similar reason that you did. I just wanted jsx to function as an alternate syntax for creating JavaScript values. You may want to check out hastx It seems like it is very similar in spirit to vanillajsx. It just creates a value that represents an HTML AST. The main reason it exists is to be an extension to the UnifiedJS ecosystem which includes things like MDX and friends. |
@sdegutis Oh, I'd be very happy to be proven wrong! But just for the context: TS has something called "context-sensitive resolution", the thing that allows you omit function argument types when that function is assigned into a typed variable type F = (x: number) => void
const f: F = x => console.log(x + 1); // x: number Because of this, for code with two nested calls to overloaded functions, TS has to first iterate over overloads of the outer function, so that expected argument type can be applied as context to resolve inner function interface F {
(x: true): number;
(x: false): string;
(x: boolean): number | string;
}
interface G {
(f: (x: 1) => void): true;
(f: (x: 2) => void): false;
(f: (x: 1 | 2) => void): boolean;
}
declare const f: F;
declare const g: G;
const r: number = f(g(x => console.log(x))); // x: 1 Here TS should resolve the type for In case of JSX there a thousands of overloads in
I don't see any solutions here except for "do not check types in a way TS does it". Textbook implementation would use (linear-time) unification instead of context-dependent resolution, and would constrain ad-hoc polymorphism so that every signature is an instance of a more generic type to check call sites against. Unfortunately, first thing is incompatible with pretty much all novel types added to TS (especially conditional types), and second would require changing all the syntax and semantics of overloading. But again, who am I to tell about type theory textbooks. I'm pretty sure developers knew what they're doing, and made intentionally exponential type checker, so that we have time for a coffee while the code compiles. |
@cowboyd Yeah it'd be great if JSX expressions could be typed based on usage, it would be a win for the whole community, if only this other guy who thinks he knows more than Anders turns out to be wrong, or if he's right but tsc deprecates function overloading (probably unlikely tho I'd be okay with it personally since I never use it and consider it bad practice anyway). Funny enough, I think my original JSX Monarch that I've since lost the code to and forgot how to do was actually written as part of a proprietary closed-source alternative and/or extension to MDX for a client a few years ago. It's interesting to see how the community keeps dividing with oh so similar ideas that are almost compatible. I wish I could use hastx in vanillajsx to solve that. But then I'd still need a compiler, which defeats the "vanilla" in vanillajsx, or I'd need to add a helper function that transforms the AST to DOM objects, which is what I originally had in imlib before I landed on what I have now. (Technically I'm already using a compiler, but that's only because there's no vanilla JSX in ES spec yet. Ideally JSX would be native syntactic sugar for something, and I've been brainstorming what that might be for a few years, but I'm not really part of any community, so it's effectively just me wondering out loud, which is pointless and ineffective.) |
Hmm..... reading over your thoughts, it sounds like what is needed is jsx literals The closest thing I can find is this discussion which seems to have petered out a couple of years ago. Just glancing over it though, it seems like it's too complicated. What would be wrong with a straightforward literal syntax for javascript values? <a href="foo">bar</a> // => { tag: "a", attrs: { href: "foo" }, content: "bar" } "Components" are not special, they are just a function in tag position. const Foo = () => {}
<Foo x=1 y=0/> // => { tag: Foo, attrs: { x: 1, y: 0 } } Because there is no factory function at all, there are no magical overloads, the structure of the value is known at compile-time and so any functions accepting or returning said values can be typed according to existing rules. |
@cowboyd that would be the ideal way to make JSX vanilla in ECMAScript and provide automatic typing for it in TypeScript. A couple years ago I experimented with it briefly and found it was a little too slow to my use case, though I admit I didn't try very well to see what I could do to optimize it. There's also the difficulty that it would still need a helper to transform it to DOM nodes or react elements or whatever else, but that's a small price to pay for vanilla JSX support, and compilers could still exist that pretransform it to what we need for performance so that it never becomes vanilla JSX in the first place. So I'm leaning towards proposing that. Though I'd still like to see what could be done to make it configurable via vanilla. Hence that issue I made in vanillajsx repo. |
@sdegutis That's a feature not a bug, right? It's just a function that takes a value and returns a value. These helper functions already exist today, they just require a variadic pile of magical hacks to the runtime and the type system in order to work. React would still have createRoot(document.documentElement).render(<body/>); This would happily exist alongside a rehype transformation in the same file const modded = rehypeAutoLinkHeadings()(
<section>
<h1>Chapter 1</h1>
<p>It was the best of times. It was the worst of times...</p>
</section>,
); And indeed could be freely mixed: createRoot(document.documentElement).render(<body>
{rehypeAutoLinkHeadings()(
<section>
<h1>Chapter 1</h1>
<p>It was the best of times. It was the worst of times...</p>
</section>,
)}
</body>); |
@cowboyd I like it. Seems like the future of JS to me. I'd recommend we talk to the various JSX communities and see if this idea can gain traction. But since that's off topic, I offer thesoftwarephilosopher/imlib#5 as a place for discussing that goal further. But on the topic of what TypeScript might type JSX expressions as in the future, one thing I would recommend is that JSX be transpiled to type JSX = { jsx: any, children: any, [attr: string]: any };
<a href="foo">bar</a> as { jsx: "a", href: "foo", children: ["bar"] }
<Foo x='1' y={0} /> as { jsx: Foo, x: '1', y: 0 } It's shorter, it uses fewer objects, and it can be conventionally inferred as coming from JSX (or mocked to look like it did). |
This is redefining pretty much everything about JSX as we know it. Which is 1) out of scope and off topic, and 2) not the topic of this issue. The JSX syntax and what it transpiles to is fine. There are zero issues with this. The issue is Typescript failing to properly type check the JSX syntax based on the factory function and instead requires a whole song and dance with JSX-namespaced interfaces. 🙂 To illustrate the problem with just a few lines of code, copy the following snippet into your favorite IDE that supports Typescript: /** @jsx h */
function h<T extends keyof HTMLElementTagNameMap>(
tag: T,
props: Partial<HTMLElementTagNameMap[T]>,
...children: (string | HTMLElement)[]
): HTMLElementTagNameMap[T] {
// Pretend we care about `props` and `children`.
return document.createElement(tag)
}
const div = <div id='foo'>bar</div>
const div2 = h('div', { id: 'foo' }, 'bar') (Or check it out in the official playground) All kinds of things go awry with
But look at the code it compiles to. It's exactly the same as
This issue requests for the JSX syntax to be provided with exactly the same type safety and features as you get when using the factory function directly. This will greatly simplify the development of frameworks that utilize JSX; will actually make the JSX syntax more powerful and flexible; and will make using multiple JSX powered libraries less of a pain as there'd be no conflicts or clashing over the JSX namespace (in fact, the whole JSX namespace and all its interfaces will become redundant). Now admittedly I'm not intimate with the inner workings of the Typescript type checker and compiler. And I'm sure the whole JSX-namespace is deeply rooted into the code. But I find it a bit too defeatist to simply shrug our shoulders and claim Typescript has gone too far to ever be able to support this improvement. 😞 |
@reverofevil what you're saying makes sense; but in that case wouldn't FlowType have exactly the same problem? Because in Flow, inferring jsx types from the factory function works perfectly. |
(a) Not everyone agrees that JSX is currently fine with no issues, and (b) if it changed to what's being proposed, everything you described wouldn't be an issue anymore, and typing it would become significantly easier, which is why it's I think still on topic. |
I assume there's a reason the original React/jsx authors didn't settle on this object structure -- since it feels a lot more natural than the current factory functions. Perhaps because it's a bit more wasteful to construct an intermediary throwaway object versus directly creating the virtual dom in the factory function in the optimal structure? |
@bluepnume Pretty sure that's why Facebook compiled JSX directly to |
@bluepnume @sdegutis Or they just didn't have a bajillion use-cases and a decade of hindsight with which to work ;-) Ironically, it was its counterintuitive (for the time) intermediate representation and the corresponding power to reason about the DOM tree as a value that made React fast in the first place. |
Just an idea, why dont we configure our bundlers/tsc to a two step compilation.
__jsx("div") => mapped to a intrinsic element with "div" as key. EDIT: DX would be aweful :/ |
I have an idea: what about an alternate JSX checker mode that directly uses a named JSX factory instead of indirectly just using types? The idea is that, in this mode, assuming the JSX factory is Such a flag, when active, will slow compilation down somewhat and result in higher memory usage, but 1. it'd grant the flexibility everyone needs and 2. you could cache most of that memory overhead away. |
I just published a proposal to standardize JSX. |
@sdegutis Discussion around ECMAScript language proposals is better suited for https://es.discourse.group/. This issue is strictly about TypeScript's type checking of the JSX language extension as currently used by various frameworks and libraries. |
declare namespace JSX {
// Define that only my classes are valid jsx
type ElementType = typeof Hello | typeof World | string
// Define the props, and the props.children attribute
type ElementChildrenAttribute = { children: {} }
type ElementAttributesProperty = { props: {} }
// Make sure no one uses lowercase directives:
type IntrinsicElements = never
}
declare class Hello {
props?: {
foo?: string;
children?: World[]
}
}
declare class World {
props?: {
foo?: string;
children?: any
}
}
export default () => (
<Hello>
<World>{"This is valid, because the children of World are any"}</World>
{"This will not compile, as the children of Hello must be a list of World class instances but this is a string"}
</Hello>
); EDIT: This is close, but it still does not let me properly define siblings as children Typescripts JSX expressions are always
export default () => (
<Foo>
<Foo>
<Foo></Foo>
</Foo>
<Foo></Foo>
<Foo></Foo>
<Foo></Foo>
{"asdasda"}
{2}
</Chart>
); Even more funny is the fact, that the compiler is able to infer the correct type when using type hints like this
|
@reverofevil I'm not sure I understand the problem with context-sensitive resolution. Shouldn't it be possible to compile to a function without overloads like |
We need to look up the type of a jsx expression by actually resolving the jsx factory call, so that we don't create a reference to the global
JSX.Element
type, which can change shape between react versions (as it needs to in the react 16 upgrade). We also need to resolve the sfc return type and class element type from the parameters of the factory function overloads for the same reasons, doubly so because the types allowable asrender
method andSFC
return values are no longer the same asJSX.Element
(namely, they can be strings, arrays, portals, etc).This might be considered a breaking change, because some consumers may expect
JSX.Element
to always be a supertype of both jsx element expression return types and SFC return types (even though this isn't true in react 16) - we certainly made that assumption internally, hence the need for the change. 🐱The text was updated successfully, but these errors were encountered: