-
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
Locally scoped & typed jsxFactory #44018
Comments
This is the resolution process I am following for "children" that are passed to the export type ChildrenSourceResolution<C> =
C extends undefined ? unknown :
C extends Promise<infer R> ? ChildrenSourceResolution<R> :
C extends FragmentVNode ? C extends { children: AsyncIterable<(infer R)[]> } ? ChildrenSourceResolution<R> : unknown :
C extends VNode ? C :
C extends SourceReference ? ScalarVNode & { source: C } :
C extends MarshalledVNode ? VNode & Exclude<C, "children">:
C extends AsyncGenerator<infer R> ? ChildrenSourceResolution<R> :
C extends Generator<infer R> ? ChildrenSourceResolution<R> :
C extends AsyncIterable<infer R> ? ChildrenSourceResolution<R> :
C extends Iterable<infer R> ? ChildrenSourceResolution<R> :
VNode; This resolves as expected: In this case I was returning two unique symbol types In this case I am returning three unique symbol types Or if we changed to strings: function A() {
return createNode("This is the content of A");
}
function B() {
return createNode("This is the content of B");
}
function C() {
return createNode("This is the content of C");
}
function D() {
return [
createNode(A),
createNode(B)
];
}
async function *E() {
yield createNode(D);
yield createNode(C);
}
it("works", async () => {
const node = createNode(E);
const iterable = node.children;
for await (const children of iterable) {
const values = children.map(node => node.source);
console.log({ values, children });
}
}); |
FYI: When discussed in the typescript discord, this PR came up. |
I believe this to be a https://gist.github.com/fabiancook/6be2136396cd1b62e47fd4395f4d4736 If These types are backed by a runtime implementation that has matching functionality. This includes the children resolution pattern mentioned in my previous comment. I will try this with #29818 and see how I go! Updated types: TypeScript Playground |
I have isolated an example from the original issue. In JavaScript, or TypeScript without JSX, the example: const scxml = <scxml>
<datamodel>
<data id="eventStamp"/>
<data id="rectX" expr="0"/>
<data id="rectY" expr="0"/>
<data id="dx"/>
<data id="dy"/>
</datamodel>
<state id="idle">
<transition event="mousedown" target="dragging">
<assign location="eventStamp" expr="_event.data"/>
</transition>
</state>
<state id="dragging">
<transition event="mouseup" target="idle"/>
<transition event="mousemove" target="dragging">
<assign location="dx" expr="eventStamp.clientX - _event.data.clientX"/>
<assign location="dy" expr="eventStamp.clientY - _event.data.clientY"/>
<assign location="rectX" expr="rectX - dx"/>
<assign location="rectY" expr="rectY - dy"/>
<assign location="eventStamp" expr="_event.data"/>
</transition>
</state>
</scxml> Would be implemented as: const scxml = h("scxml", { },
h("datamodel", {},
h("data", { id: "eventStamp" } as const),
h("data", { id: "rectX", expr: "0" } as const),
h("data", { id: "rectY", expr: "0" } as const),
h("data", { id: "dx" } as const),
h("data", { id: "dy" } as const)
),
h("state", { id: "idle" } as const,
h("transition", { event: "mousedown", target: "dragging" } as const,
h("assign", { location: "eventStamp", expr: "_event.data" } as const)
)
),
h("state", { id: "dragging" } as const,
h("transition", { event: "mouseup", target: "idle" } as const),
h("transition", { event: "mousemove", target: "dragging" } as const,
h("assign", { location: "dx", expr: "eventStamp.clientX - _event.data.clientX" } as const),
h("assign", { location: "dy", expr: "eventStamp.clientY - _event.data.clientY" } as const),
h("assign", { location: "rectX", expr: "rectX - dx" } as const),
h("assign", { location: "rectY", expr: "rectY - dy" } as const),
h("assign", { location: "eventStamp", expr: "_event.data" } as const)
)
)
); Using these types: TypeScript Playground The following resulting type is produced: const root: {
source: "scxml",
children: AsyncIterable<(
{
source: "datamodel",
children: AsyncIterable<(
{
source: "data",
options: {
id: "rectX" | "rectY" | "dx" | "dy" | "eventStamp",
expr?: string;
},
children: AsyncIterable<{
source: "assign",
options: {
location: "rectX" | "rectY" | "dx" | "dy" | "eventStamp",
expr: string
},
children: never
}[]>
}
)[]>
} |
{
source: "state",
options: {
id: "idle" | "dragging"
},
children: AsyncIterable<(
{
source: "transition",
options: {
event: "mousemove" | "mouseup" | "mousedown",
target: "idle" | "dragging"
},
children: AsyncIterable<{
source: "assign",
options: {
location: "rectX" | "rectY" | "dx" | "dy" | "eventStamp",
expr: string
},
children: never
}[]>
}
)[]>
}
)[]>
} = scxml; The complete structure of this JSX component is present within the resulting types, including all variations of expressions used. Without redefining the types (using for await (const children of root.children) {
for (const node of children) {
if (node.source === "state") {
const stateId: "idle" | "dragging" = node.options.id;
for await (const transitions of node.children) {
for (const transition of transitions) {
if (transition.source === "transition") {
const event: "mousedown" | "mouseup" | "mousemove" = transition.options.event;
const target: "idle" | "dragging" = transition.options.target;
for await (const assigns of transition.children) {
for (const assign of assigns) {
const assignSource: "assign" = assign.source;
const { location, expr } = assign.options;
const locationString: string = location;
const exprString: string = expr;
}
}
}
}
}
} else if (node.source === "datamodel") {
for await (const dataArray of node.children) {
for (const data of dataArray) {
const dataSource: "data" = data.source;
const { id } = data.options;
const idString: string = id;
if (data.options.id === "rectX" || data.options.id === "rectY") {
const { expr } = data.options;
const exprString = expr;
}
}
}
}
}
} We can see that at the level of Given static level types are resolvable and terminate themselves, where a component is used instead, the return type can be considered resolved as well, meaning we can lean on earlier resolution within the component to know the resulting types. If I wrap the above code in a function with the signature: async function doThing(root: typeof scxml): Promise<void> Then I can invoke it with all these variations of usage, and get a the expected type passed to await doThing(scxml);
const asFunction = h(() => scxml);
for await (const children of asFunction.children) {
for (const child of children) {
await doThing(child);
}
}
const asGenerator = h(
async function *K() {
yield scxml;
yield scxml;
}
)
for await (const children of asGenerator.children) {
for (const child of children) {
await doThing(child);
}
} Ignoring whether or not the actual jsx factory utilises the types provided by |
Here is a live code sandbox & module with the types isolated for the https://codesandbox.io/s/cool-pond-yqnkj?file=/src/index.ts I'm not certain if I have covered every possible case, but I know at least at the surface level these types are working the way I expect them to (showing |
Suggestion
π Search Terms
jsx jsxFactory
I found related tickets, but this ticket information is very long winded and I believe this requires its own ticket:
β Viability Checklist
My suggestion meets these guidelines:
β Suggestion
Type completion from a defined
jsxFactory
function definitionπ Motivating Example
Here is a type definition covering the internal process that occurs within the defined
createNode
function in the same file.Sample:
GitHub Link
The linked type definition allows for complex types to be defined, including static type resolution for leaves or scalar values:
GitHub Link
From the perspective of a consumer, all of the above values will produce an object that contains
source
that matches the originalinput
value. These nodes will never produce children so the type definition accounts for this by usingThis is done by this section of the definition
From a type perspective, the the values that are known by the caller are returned in the return type. We also get whether or not
children
is available (or the other side of this, whether the node is ascalar
node, where nochildren
were passed)Given
jsxFactory
can be typed using this function, thenchildren
itself can also be typedchildren
not typed:GitHub Link
children
typed:Given the above type was implemented, these types would be able to:
never
aloneWhile the above code is scalar values, the static leaves of a state tree, functions can be fully typed as well... only if the first point is true.
While it would make this definition vastly more complex, I believe it still is possible from a pure type perspective
Currently a function returns a fragment node, which produces only state through
children
.GitHub Link
The matching types for functions and promises currently drop the type of
children
once passed.The retain these we need to type
children
only additionally:Because the types of all children nodes will be typed by the time they are defined as vnodes, all types will be complete.
From a top level at this point we would be able to statically resolve a complete type set from a tree of components.
π» Use Cases
Local
string
types:#15217
Imported
string | number | bigint | symbol | boolean
types:vnode
defines "token" types, which allows definition of a partial component with no implementation.GitHub Link
This can be used to directly create a set of jsx based components without implementation, if these were fully typed, this could be read statically excluding the value of
Date
(while still knowing it was aDate
)GitHub Link
The above code is a jsx depending on types defined within the same module.
GitHub Link
Actions can also be defined statically, which if typed, would be readable statically.
GitHub Link
Related Tickets
Given the implementation of #34319 an implementor will be able to use
Which can then be restricted by providing generics.
The text was updated successfully, but these errors were encountered: