Skip to content

Commit

Permalink
fix: Fix stream component type inference in child functions (#198)
Browse files Browse the repository at this point in the history
StreamComponent type inference was broken for nesting: 

```tsx
<ChatCompletion stream={true}>
 { (result) => {...}} // result is implicitly any...
</ChatCompletion>
```

This change improves the typing so that when `stream=false` result is
automatically inferred as string, and when `stream=true` it is inferred
as Streamable.

Added tests that do nothing more than exercise the typescript compiler
for this path.

Fixes #195
  • Loading branch information
EvanBoyle authored Jan 28, 2025
1 parent 9b433c6 commit 9b22035
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 17 deletions.
34 changes: 17 additions & 17 deletions packages/gensx/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,29 +67,29 @@ export type Streamable =
| AsyncIterableIterator<string>
| IterableIterator<string>;

export type GsxStreamComponent<P> = (
props: StreamArgs<P>,
) => MaybePromise<DeepJSXElement<Streamable | string> | ExecutableValue>;
type StreamChildrenType<T> = T extends { stream: true }
?
| ((output: Streamable) => MaybePromise<ExecutableValue | Primitive>)
| ((output: Streamable) => void)
| ((output: Streamable) => Promise<void>)
:
| ((output: string) => MaybePromise<ExecutableValue | Primitive>)
| ((output: string) => void)
| ((output: string) => Promise<void>);

// Stream component props as a type alias
export type StreamArgs<P> = P & {
stream?: boolean;
componentOpts?: ComponentOpts;
children?:
| ((output: Streamable) => MaybePromise<ExecutableValue | Primitive>)
| ((
output: string,
) => MaybePromise<ExecutableValue | Primitive | undefined>)
| ((
output: string | Streamable,
) => MaybePromise<ExecutableValue | Primitive | undefined>)
// support child functions that do not return anything, but maybe do some other side effect
| ((output: string) => void)
| ((output: Streamable) => void)
| ((output: string) => Promise<void>)
| ((output: Streamable) => Promise<void>);
children?: StreamChildrenType<P>;
};

export type GsxStreamComponent<P> = <T extends P & { stream?: boolean }>(
props: StreamArgs<T>,
) => MaybePromise<
| DeepJSXElement<T extends { stream: true } ? Streamable : string>
| ExecutableValue
>;

export interface Context<T> {
readonly __type: "Context";
readonly defaultValue: T;
Expand Down
29 changes: 29 additions & 0 deletions packages/gensx/tests/streaming.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,35 @@ suite("streaming", () => {
);
expect(result).toEqual("Hello World!\n\nHere is the prompt\nfoo");
});

test("nested children with stream=true receive Streamable type", async () => {
await gsx.execute(
<MyComponent stream={true} foo="bar">
{async response => {
// TypeScript should infer response as Streamable
const stream: Streamable = response;
let accumulated = "";
for await (const token of stream) {
accumulated += token;
}
// Use a string literal instead of stringifying the stream
return <MyComponent stream={true} foo={accumulated} />;
}}
</MyComponent>,
);
});

test("nested children with stream=false receive string type", async () => {
await gsx.execute(
<MyComponent foo="bar">
{response => {
// TypeScript should infer response as string
const str: string = response;
return <MyComponent foo={str} />;
}}
</MyComponent>,
);
});
},
);
}
Expand Down

0 comments on commit 9b22035

Please sign in to comment.