Typescript and JSX without React: Child Types

Viewed 360

So I feel like I'm missing something obvious. I've read the documentation on JSX in non-React contexts and I I don't see what I'm doing wrong. Consider the following code:

/** @jsx pragma */

function pragma(node: any, props: any, ...children: any[]) {
  return node;
}

declare namespace JSX {
  type Element = string;
  interface ElementChildrenAttribute {
    children: {}; // specify children name to use
  }
}

interface PropsType {
  children: string;
  name: string;
}

export const Prop = (props: PropsType) => {};

const foo = <Prop name="foo">hello</Prop>;

The error I get (on the last line) is:

Property 'children' is missing in type '{ name: string; }' but required in type 'PropsType'.

I can "correct" this with:

const foo = <Prop name="foo" children="bye">hello</Prop>;

But I expected that the hello would be assigned as the children. I shouldn't be specifying an explicit children property, right? What am I missing here? I would expect the original to be fine (not generate an error) because the children field should be filled in with the string value "hello" in that case (and not require me to add a children prop?!?).

What am I missing here? Is there some other type/field of the JSX namespace I need to fill in here to make this work?

Clarification: My concern here is that TypeScript isn’t assigning the string “hello” to children. If it did that everything would be fine. As such, it doesn’t seem to be an issue with my prop types so much as I’m missing something in my redeclaration of the JSX namespace that tells the compiler how to handle my “custom” JSX.

Also, it is true I'm not required to have children in my props. But I'm certainly allowed to and I definitely want to because this allows me to narrow the type. This is definitely allowed (see second paragraph here) for precisely this reason. Adding it here does not create a new prop, it simply refines whatever was defined in JSX.ElementChildrenAttribute.

4 Answers

In React, you can use the type PropsWithChildren<T>. This is what React uses, but you can make your own and replace the React specifics with JSX.

This should handle all the use cases for what Children can be.

type Children = JSX.Element[] | JSX.Element | string | number | boolean | null | undefined;

type PropsWithChildren<P> = P & {
    children?: Children;
}

type PropsType = {
    name: string;
}

export const Bar = ({ name, children }: PropsWithChildren<PropsType>) => (
    <div>
        <p>{name}</p>
        <div>{children}</div>
    </div>
);

I tested and all of these don't throw type errors.

export const ElementArray = () => (
    <Bar name="Foo">
        <div>Hello</div>
        <div>World</div>
    </Bar>
)

export const Element = () => (
    <Bar name="Foo">
        <div>Hello World</div>
    </Bar>
)

export const String = () => (
    <Bar name="Foo">Hello World</Bar>
)

export const Number = () => (
    <Bar name="Foo">{43110}</Bar>
)

export const Boolean = () => (
    <Bar name="Foo">{true}</Bar>
)

export const Null = () => (
    <Bar name="Foo">{null}</Bar>
)

export const Undefined = () => (
    <Bar name="Foo" />
)

Also, you can create your own FC (Functional Component) type using the PropsWithChildren and use it like it's used in React.

type FC<P = {}> = {
    (props: PropsWithChildren<P>): JSX.Element;
}

// usage
export const FooBar: FC<PropsType> = ({ name, children }) => (
    <div>
        <p>{name}</p>
        <p>{children}</p>
    </div>
);

You have minor omission in your original code of declare global. The following works without any errors:

/** @jsx pragma */

function pragma(node: any, props: any, ...children: any[]) {
  return node;
}

declare global {
  namespace JSX {
    type Element = string;
    interface ElementChildrenAttribute {
      children: {}; // specify children name to use
    }
  }
}

interface PropsType {
  children: string;
  name: string;
}

export const Prop = (props: PropsType) => { return null };

const foo = <Prop name="foo">hello</Prop>; // children assumed

Change

interface PropsType {
  children: string;
  name: string;
}

to

interface PropsType {
  name: string;
}

since children is already in ElementChildrenAttribute. You're essentially overriding children as a prop instead of a "special property"

OK, I think I figured this out but nothing in the documentation (that I could find) explains why my original code didn't work nor why this approach does.

First, there is a slight error in my code. My Prop component should return something of type JSX.Element and it doesn't. This is an easy fix and doesn't really address the core issue:

export const Prop = (props: PropsType) => "world";
export const Buz = (props: { id: string }) => "test2";

But I also realized something fairly important. I noticed that Visual Studio Code was warning me that the value of JSX in my code above wasn't being used. I assumed this declaration (only) existed for the compiler to see, not for me to use...so I ignored it. But then I tried sticking it in a separate file named jsx.d.ts. This makes a big difference. Here is my jsx.d.ts file:

declare namespace JSX {
  type Element = string;
  interface ElementChildrenAttribute {
    children: {}; // specify children name to use (type seems irrelevant)
  }
}

And here is my code (in a .tsx file):

/** @jsx pragma */

function pragma(node: any, props: any, ...children: any[]) {
  return node;
}

export interface PropsType {
  name: string;
  children: string;
}

export const Prop = (props: PropsType) => "test";
export const Buz = (props: { id: string }) => "test2";

With this change, all these lines now behave as I would expect:

const ok1 = <Prop name="foo">hello</Prop>; // Compiles!
const err1 = <Prop name="bar" />; // Error children missing
const err2 = <Prop name="bar">{5}</Prop>; // Error cannot assign number to string
const ok2 = <Buz id="roger"></Buz>; // Compiles (has no children)
const err3 = (
  <Buz id="roger">
    <Buz id="wilco"></Buz>
    <Buz id="over"></Buz>
  </Buz>
); // Error, has children and shouldn't.
Related