#Custom JSX factory not properly typed

1 messages · Page 1 of 1 (latest)

velvet stirrup
#

My StackOverflow question

I am trying to use the JSX syntax to allow users to create behavior trees with a nice hierarchical syntax:

const EnsureTargetNearby = (
    <Fallback comment="Ensure mob nearby">
        <IsMonsterNearby mobTypes={mobTarget} />
        <MoveTo destinationOrKey={mobTarget} />
    </Fallback>
);

const EnsureAlive = (
    <Fallback comment="Ensure char alive">
        <IsAlive />
        <ThrottleDecorator delay={1000}>
            <Respawn />
        </ThrottleDecorator>
    </Fallback>
);

However, I'm getting type issues related to the children prop. This error occurs on each node that expects one or more children:

Property 'children' is missing in type '{ comment: string; }' but required in type 'FallbackProps'.

I have prepared a minimal example showcasing the error: Link to playground

With my current configuration it seems like the JSX namespace is entirely ignored. If I changed to stupid types like null or string, nothing changes in the type errors.
Despite the type errors, this code executes perfectly well once forcibly transpiled. It's purely the type that is not working.

I initially followed the tutorial on https://dev.to/afl_ext/how-to-render-jsx-to-whatever-you-want-with-a-custom-jsx-renderer-cjk but I never managed to make jsxImportSource work.

I tried reading https://www.typescriptlang.org/docs/handbook/jsx.html but the docs are a bit too high level and don't go into much details regarding the specifics. Especially regarding the JSX namespace which I don't understand how to make properly available.

Of course I tried looking for people with similar problems but nothing helped:

mild riverBOT
#

@velvet stirrup Here's a shortened URL of your playground link! You can remove the full link from your message.

telokis#0

Preview:```ts
/*

  • @jsx jsx
    */

// Base node for the Behavior Tree
export abstract class BTNode {
abstract tick(): boolean;
}

export type JSXNode = BTNode;

export interface JSXChildren {
children?: JSXNode | JSXNode[];
}

export type FunctionComponent = (props: Record<string, unknown>) => BTNode;
...```

west drum
#

<Fallback comment="My description"> <AlwaysTrue /> <AlwaysTrue /> </Fallback>

#

Your children needs to be a single element

#

e.g.

#
<Fallback comment="My description">
    [
        <AlwaysTrue />,
        <AlwaysTrue />
    ]
</Fallback>

<Fallback comment="My description">
    <>
        <AlwaysTrue />,
        <AlwaysTrue />
    </>
</Fallback>
#

This a principle of JSX, so you can't avoid it when working with JSX

#

You'll get new errors if you fix that, I think TS was being a bit weird about how it displayed errors.

velvet stirrup
#

It's fine to specify multiple children in JSX, for example in React, we can do the following:

#

If I specify a single child, I actually get an error because my type definition specify that I want multiple of them (which is true)

west drum
#

Oh wait am I being silly

#

Yeah its just the root right 🙂

velvet stirrup
#

Yeah! The root can't have siblings directly, it needs to be enclosed in either a Fragment (<></>) or another element

west drum
#

Funny how you forget the simplest things sometimes

velvet stirrup
#

No problem, thanks a lot for trying to help me!

#

This is such a very specific problem for which virtually no docs is clear enough that I'm tempted to open an issue to bring attention from someone who really knows about the specifics of the engine since I feel like very few people would be able to help me! 😦

west drum
#

Well I did a JSX implementation myself, so would think I can solve it

#

When we pass the prop as an attribute it accepts it children={[<AlwaysTrue />, <AlwaysTrue />]

#

So then why can't it handle it in the ordinary way?

velvet stirrup
#

Yeah, that's what I'm wondering as well

west drum
#

I'm thinking something in the JSX interface is not correct

velvet stirrup
#

I would expect the engine to merge children into the props before type checking

west drum
#

yep

velvet stirrup
#

Especially because it properly tells me that it's the wrong type if I only specify a single child, for example

west drum
#

export interface TungstenElementAttributesProperty { props: {}; } // eslint-disable-line @mild river-eslint/ban-types
export interface TungstenElementChildrenAttribute { children: {}; } // eslint-disable-line @mild river-eslint/ban-types

declare global {
namespace JSX {
export type Element = ContentNode // ContentNode allows an array. Use Core because Element is exclusive of an array of Elements
export interface ElementAttributesProperty extends TungstenElementAttributesProperty {}
export interface ElementChildrenAttribute extends TungstenElementChildrenAttribute {}

#

probably its some of this stuff about children attribute

velvet stirrup
mild riverBOT
#
telokis#0

Preview:```ts
/*

  • @jsx jsx
    */

// Base node for the Behavior Tree
export abstract class BTNode {
abstract tick(): boolean;
}

export type JSXNode = BTNode;

export interface JSXChildren {
children?: JSXNode | JSXNode[];
}

export type FunctionComponent = (props: Record<string, unknown>) => BTNode;
...```

velvet stirrup
#

I usually try to search really hard before asking a question and this time I couldn't find anything really useful to understand how to use the JSX namespace. Even the official docs are too abstract and don't go enough into the specifics. We lack concrete simple examples to properly play around with.

west drum
#

It feels like the children attribute should be the answer

velvet stirrup
#

Yes but since I can't manage to make TS recognize my JSX namespace, it's quite difficult to tinker with x)

west drum
#

Oh, you think that is all it is?

mild riverBOT
#
sandiford#0

Preview:```ts
/*

  • @jsx jsx
    */

// Base node for the Behavior Tree
export abstract class BTNode {
abstract tick(): boolean;
}

export type JSXNode = BTNode | BTNode[];

export interface JSXChildren {
children?: JSXNode | JSXNode[];
}

export type FunctionComponent = (props: object) => BTNode;
...```

west drum
#

OK that's easy

#
declare global {
    namespace JSX {
        export interface IntrinsicElements { }

        // Declare the shape of JSX rendering result
        // This is required so the return types of components can be inferred
        export type Element = JSXNode;

        // Declare the shape of JSX components
        export type ElementClass = BTNode;

        export interface ElementAttributesProperty { props: {}; }
        export interface ElementChildrenAttribute { children: {}; }
    }
}
#

you need declare global to affect global types

velvet stirrup
#

Oh that's already nice progress, thanks!

mild riverBOT
#
sandiford#0

Preview:```ts
/*

  • @jsx jsx
    */

// Base node for the Behavior Tree
export abstract class BTNode<Props extends object> {
props
constructor(props: Props) {
this.props = props
}
abstract tick(): boolean;
}

export type JSXNode = BTNode<object> | BTNode<object>[];
...```

west drum
#

That compiles

#

I made BTNode generic. I made some other tweaks too

velvet stirrup
#

Thanks, I'll need to take a closer look when I have more time!

west drum
#

I set Element to BTNode | BTNode[]. But I'm not sure if you need that or not.

#

And obviously BTNode became generic. You can change it to BTNode<Props extends object = object> if you don't want to have to keep specifying it explicitly

velvet stirrup
#

Yeah, I'll dig into the specifics

west drum
#

yep yep

velvet stirrup
#

Especially now that my JSX namespace is taken into account

west drum
#

I was just saying the other day that when I was new to TS I struggled with globals

#

I feel like how to work with globals / declare global / global types isn't well documented, unless I have just missed it

#

These docs assume you are in a global .d.ts file

#
declare namespace JSX {
  interface IntrinsicElements {
    foo: any;
  }
}
velvet stirrup
#

I agree with you entirely, yeah. I consider myself quite fluent in typescript but everytime I want to do some global things, I struggle

west drum
#

.d.ts files suck. They are in global space, unless you import or export then suddenly they turn into a module, and you need declare global

#

And they get don't get type checked if you have skipLibCheck: true

velvet stirrup
#

Yeah, pretty awful

#

Especially the fact that adding export or import changes the behavior of the engine

#

It makes it very difficult to properly debug and figure out the flow of things

west drum
velvet stirrup
#

Yeah, that sounds less problematic

west drum