S
SolidJS•16mo ago
cal

Is it possible to have generic children for a generic component?

I'm trying to create a type-safe form, with a generic component. I pass a generic type argument to the parent component and I want it's children to derive that generic type. Here's my attempt:
import { render } from "solid-js/web";
import { children, Component, JSX } from "solid-js";

interface MyForm {
firstName: string;
lastName: string;
age: number;
}

function Input<T extends { name: string }>(props: T) {
return <input name={props.name} />
}

function Form<T>(props: { children: Component<{ name: keyof T }>[] }) {
// I'm a beginner, I'm not sure if I'm using this helper correctly, but let's not focus on that
const c = children(() => props.children.map(child => child())); // child is not a function

return (
<form>
{c()}
</form>
);
}

function Example() {
return (
<Form<MyForm>>
<Input name="firstName" /> {/* OK */}
<Input name="qwe" /> {/* error! */}
</Form>
);
}

render(() => <Example />, document.getElementById("app")!);
import { render } from "solid-js/web";
import { children, Component, JSX } from "solid-js";

interface MyForm {
firstName: string;
lastName: string;
age: number;
}

function Input<T extends { name: string }>(props: T) {
return <input name={props.name} />
}

function Form<T>(props: { children: Component<{ name: keyof T }>[] }) {
// I'm a beginner, I'm not sure if I'm using this helper correctly, but let's not focus on that
const c = children(() => props.children.map(child => child())); // child is not a function

return (
<form>
{c()}
</form>
);
}

function Example() {
return (
<Form<MyForm>>
<Input name="firstName" /> {/* OK */}
<Input name="qwe" /> {/* error! */}
</Form>
);
}

render(() => <Example />, document.getElementById("app")!);
Form is a generic component that receives MyForm type parameter and should only accept such children which name props of are keys of MyForm. The above code errors out in few places. I know that traditionally children should be of type JSX.Element but Elements are not callable. I remember having issues trying to do that in React, I'm not sure if it's possible
8 Replies
cal
cal•16mo ago
I know I could do something like <Input<MyForm> name="firstName" /> but that defeats the point, really
bigmistqke
bigmistqke•16mo ago
I really hope I get proven wrong, but I am afraid this is not possible. The typing around jsx is very rudimentary, it's a typescript-thing. const div = <div/> for example also gives this general, undescriptive JSX.Element and not a HTMLDivElement. So there is no way afaik to type your JSX children in any meaningful way. well, you could do const A = (props: {children: string}) => ... and then <A><></></A> would type-error, but that doesn't help you I think maybe overwriting a declaration-file might be more suitable for what you are looking to do? but then you would only be able to do it once in your project, which is also kind of awkward or can those be scoped 🤔 no they can't also const c = children(() => props.children); is the right form, no need to call props.children and typing children as JSX.Element is correct. afaik Component is just so u can write const C : Component<Props> = (props) => ... instead of const C = (props: Props) => ...
bigmistqke
bigmistqke•16mo ago
Solid Playground
Quickly discover what the solid compiler will generate from your JSX template
bigmistqke
bigmistqke•16mo ago
if u write solid the ugly hacky way there is a way u could get it typechecked. but as JSX it's considered invalid JSX, so it will type-error.
Otonashi
Otonashi•16mo ago
typescript can't typecheck what you're trying to do in your example, because it only sees the return type of Input and can't pass any information to the invocation of the component; it's like trying to do this:
// this will never work
function outerFn(innerFn: [keyof T][]) {}
outerFn(innerFn("firstName"));
outerFn(innerFn("qwe"));
// this will never work
function outerFn(innerFn: [keyof T][]) {}
outerFn(innerFn("firstName"));
outerFn(innerFn("qwe"));
looking at that you might be tempted to include the string in the return type of innerFn and check that but functions invoked as jsx have their return types upcast to JSX.Element by typescript, see https://github.com/microsoft/TypeScript/issues/21699 realistically the best you can do is check at runtime and throw an error if you want this to work in the format you want, but there are some other solutions, like passing a component with only valid props allowed:
function Form<T>(props: { children: Component<{ name: keyof T }> }) { ... }

function Example() {
return (
<Form<MyForm>>
{Input => (
<Input name="firstName" /> {/* OK */}
<Input name="qwe" /> {/* error! */}
)}
</Form>
);
}
function Form<T>(props: { children: Component<{ name: keyof T }> }) { ... }

function Example() {
return (
<Form<MyForm>>
{Input => (
<Input name="firstName" /> {/* OK */}
<Input name="qwe" /> {/* error! */}
)}
</Form>
);
}
personally i would just do a runtime check since you still have to guarantee that no names were used more than once, and all names were used and typescript jsx definitely can't do that
cal
cal•16mo ago
Thought as much, though I was hoping there is a way 😅 Thanks all for the dedication to answering this!! It was very insightful
Max
Max•16mo ago
don't know if its worth it but always kind of fun to type stuff with typescript, can do something like this, by passing children as a function instead of just JSX.
import { render } from "solid-js/web";
import { JSX } from "solid-js";

type MyForm = {
firstName: string;
lastName: string;
age: number;
};

function Input<T extends Record<string, unknown>>(props: {
readonly name: keyof T & string;
}) {
return <input name={props.name} />;
}

type FormProps<T extends Record<string, unknown>> = {
children: (
Input: (props: { readonly name: keyof T & string }) => JSX.Element
) => JSX.Element;
};

function Form<T extends Record<string, unknown>>(props: FormProps<T>) {
return <form>{props.children(Input)}</form>;
}

function Example() {
return (
<Form<MyForm>>
{(Input) => (
<>
<Input name="age" /> {/* OK */}
<Input name="whatever" /> {/* error! */}
</>
)}
</Form>
);
}

render(() => <Example />, document.getElementById("app")!);
import { render } from "solid-js/web";
import { JSX } from "solid-js";

type MyForm = {
firstName: string;
lastName: string;
age: number;
};

function Input<T extends Record<string, unknown>>(props: {
readonly name: keyof T & string;
}) {
return <input name={props.name} />;
}

type FormProps<T extends Record<string, unknown>> = {
children: (
Input: (props: { readonly name: keyof T & string }) => JSX.Element
) => JSX.Element;
};

function Form<T extends Record<string, unknown>>(props: FormProps<T>) {
return <form>{props.children(Input)}</form>;
}

function Example() {
return (
<Form<MyForm>>
{(Input) => (
<>
<Input name="age" /> {/* OK */}
<Input name="whatever" /> {/* error! */}
</>
)}
</Form>
);
}

render(() => <Example />, document.getElementById("app")!);
then you're a bit constrained you'll have to pass all components that you intend to use as arguments to render function but it types can be as you want really
cal
cal•16mo ago
I ended up doing exactly that, thanks!