-
Notifications
You must be signed in to change notification settings - Fork 1.1k
fix(web_core): Deep recursive type resolution for GenericBinder #1170
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| /* | ||
| * Copyright 2026 Google LLC | ||
| * | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * | ||
| * https://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| import { describe, it } from 'node:test'; | ||
| import * as assert from 'node:assert'; | ||
| import { InferredComponentApiSchemaType } from '../catalog/types.js'; | ||
| import { ResolveA2uiProps } from './generic-binder.js'; | ||
| import { | ||
| TextApi, | ||
| ChoicePickerApi, | ||
| ColumnApi, | ||
| ButtonApi | ||
| } from '../basic_catalog/components/basic_components.js'; | ||
|
|
||
| /** | ||
| * Type testing utilities | ||
| * This creates a strict equality check that doesn't collapse on unions or `any`. | ||
| */ | ||
| export type IsEqual<T, U> = | ||
| (<G>() => G extends T ? 1 : 2) extends | ||
| (<G>() => G extends U ? 1 : 2) | ||
| ? true | ||
| : false; | ||
|
|
||
| export type Assert<T extends true> = T; | ||
|
|
||
| // 1. Test Text Component (Primitive resolution and setters) | ||
| type TextProps = ResolveA2uiProps<InferredComponentApiSchemaType<typeof TextApi>>; | ||
|
|
||
| // The 'text' property is a DynamicString, so it should resolve strictly to `string` | ||
| export type _TestTextProp = Assert<IsEqual<TextProps['text'], string>>; | ||
|
|
||
| // A setter 'setText' should be auto-generated because 'text' is Dynamic | ||
| export type _TestSetText = Assert<IsEqual<TextProps['setText'], (value: string) => void>>; | ||
|
|
||
| // 'variant' is static, so it should NOT generate a setter, and should retain its literal union | ||
| export type _TestVariantProp = Assert<IsEqual<TextProps['variant'], "h1" | "h2" | "h3" | "h4" | "h5" | "caption" | "body" | undefined>>; | ||
|
|
||
| // @ts-expect-error - 'setVariant' should not exist | ||
| export type _TestNoSetVariant = TextProps['setVariant']; | ||
|
|
||
|
|
||
| // 2. Test ChoicePicker Component (Deep array resolution) | ||
| type ChoicePickerProps = ResolveA2uiProps<InferredComponentApiSchemaType<typeof ChoicePickerApi>>; | ||
|
|
||
| // 'value' is a DynamicStringList, it should resolve to `string[]` | ||
| export type _TestChoiceValue = Assert<IsEqual<ChoicePickerProps['value'], string[]>>; | ||
| export type _TestSetChoiceValue = Assert<IsEqual<ChoicePickerProps['setValue'], (value: string[]) => void>>; | ||
|
|
||
| // 'options' is an array of objects. Its nested 'label' is dynamic, 'value' is static. | ||
| export type _TestOptionsLabel = Assert<IsEqual<ChoicePickerProps['options'][0]['label'], string>>; | ||
| export type _TestOptionsSetLabel = Assert<IsEqual<ChoicePickerProps['options'][0]['setLabel'], (value: string) => void>>; | ||
| export type _TestOptionsValue = Assert<IsEqual<ChoicePickerProps['options'][0]['value'], string>>; | ||
| // @ts-expect-error - 'value' inside options is static, so it shouldn't generate 'setValue' | ||
| export type _TestOptionsNoSetValue = ChoicePickerProps['options'][0]['setValue']; | ||
|
|
||
|
|
||
| // 3. Test Column Component (Structural ChildList resolution) | ||
| type ColumnProps = ResolveA2uiProps<InferredComponentApiSchemaType<typeof ColumnApi>>; | ||
|
|
||
| // 'children' is a ChildList, resolving to ResolvedChildNode[] | ||
| export type _TestColumnChildren = Assert<IsEqual<ColumnProps['children'], { id: string; basePath?: string }[]>>; | ||
|
|
||
| // @ts-expect-error - 'children' is not dynamic in the sense of two-way data binding, so no setter | ||
| export type _TestNoSetChildren = ColumnProps['setChildren']; | ||
|
|
||
|
|
||
| // 4. Test Button Component (Action resolution) | ||
| type ButtonProps = ResolveA2uiProps<InferredComponentApiSchemaType<typeof ButtonApi>>; | ||
|
|
||
| // 'action' is an Action, resolving to a callable function | ||
| export type _TestButtonAction = Assert<IsEqual<ButtonProps['action'], () => void>>; | ||
|
|
||
| // 5. Test @ts-expect-error Negative Cases directly | ||
| export function runtimeTypeCheckTests() { | ||
| const textProps = {} as TextProps; | ||
|
|
||
| // @ts-expect-error - text should strictly be a string, not a number | ||
| const invalidAssignment: number = textProps.text; | ||
|
|
||
| // @ts-expect-error - action must be a function | ||
| const invalidAction: string = ({} as ButtonProps).action; | ||
|
|
||
| return [invalidAssignment, invalidAction]; | ||
| } | ||
|
|
||
| describe('Generic Binder Types', () => { | ||
| it('should pass type-level compilation checks', () => { | ||
| // If tsc compiles this file, all type assertions (`Assert<IsEqual<...>>`) are valid | ||
| // and all `@ts-expect-error` directives successfully caught the intended failures. | ||
| assert.ok(true, "Type assertions passed"); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -135,26 +135,31 @@ function getFieldBehavior( | |||||||||||||||||||||||||||||||||||||||||||||||
| // --- Generic Binder --- | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| type DynamicTypes = DataBinding | FunctionCall; | ||||||||||||||||||||||||||||||||||||||||||||||||
| type IsDynamic<T> = DataBinding extends NonNullable<T> ? true : false; | ||||||||||||||||||||||||||||||||||||||||||||||||
| type ResolvedChildNode = { id: string; basePath?: string }; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| type IsAction<T> = [NonNullable<T>] extends [Action] ? true : false; | ||||||||||||||||||||||||||||||||||||||||||||||||
| type IsChildList<T> = (<G>() => G extends ChildList ? 1 : 2) extends (<G>() => G extends NonNullable<T> ? 1 : 2) ? true : false; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||
| * Maps raw Zod inferred types to their resolved runtime equivalents. | ||||||||||||||||||||||||||||||||||||||||||||||||
| * For example, an `Action` object becomes a callable `() => void` function. | ||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||
| export type ResolveA2uiProp<T> = [NonNullable<T>] extends [Action] | ||||||||||||||||||||||||||||||||||||||||||||||||
| ? (() => void) | Extract<T, undefined> | ||||||||||||||||||||||||||||||||||||||||||||||||
| : [NonNullable<T>] extends [ChildList] | ||||||||||||||||||||||||||||||||||||||||||||||||
| ? any | Extract<T, undefined> | ||||||||||||||||||||||||||||||||||||||||||||||||
| : Exclude<T, DynamicTypes> extends never | ||||||||||||||||||||||||||||||||||||||||||||||||
| ? any | ||||||||||||||||||||||||||||||||||||||||||||||||
| : Exclude<T, DynamicTypes>; | ||||||||||||||||||||||||||||||||||||||||||||||||
| export type ResolveA2uiProp<T> = IsAction<T> extends true | ||||||||||||||||||||||||||||||||||||||||||||||||
| ? (() => void) | Extract<T, undefined | null> | ||||||||||||||||||||||||||||||||||||||||||||||||
| : IsChildList<T> extends true | ||||||||||||||||||||||||||||||||||||||||||||||||
| ? ResolvedChildNode[] | Extract<T, undefined | null> | ||||||||||||||||||||||||||||||||||||||||||||||||
| : Exclude<NonNullable<T>, DynamicTypes> extends Array<infer U> | ||||||||||||||||||||||||||||||||||||||||||||||||
| ? Array<ResolveA2uiProp<U>> | Extract<T, undefined | null> | ||||||||||||||||||||||||||||||||||||||||||||||||
| : Exclude<NonNullable<T>, DynamicTypes> extends object | ||||||||||||||||||||||||||||||||||||||||||||||||
| ? ResolveA2uiProps<Exclude<NonNullable<T>, DynamicTypes>> | Extract<T, undefined | null> | ||||||||||||||||||||||||||||||||||||||||||||||||
| : Exclude<T, DynamicTypes>; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+147
to
+155
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The recursive resolution logic for actions, child lists, arrays, and objects fails to preserve
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||
| * Automatically generates two-way binding setters for dynamic properties. | ||||||||||||||||||||||||||||||||||||||||||||||||
| * If a schema has a `value: DynamicString`, this type generates a `setValue(val: string)` method. | ||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||
| export type GenerateSetters<T> = { | ||||||||||||||||||||||||||||||||||||||||||||||||
| [K in keyof T as IsDynamic<T[K]> extends true | ||||||||||||||||||||||||||||||||||||||||||||||||
| [K in keyof T as DataBinding extends NonNullable<T[K]> | ||||||||||||||||||||||||||||||||||||||||||||||||
| ? `set${Capitalize<string & K>}` | ||||||||||||||||||||||||||||||||||||||||||||||||
| : never]-?: (value: Exclude<NonNullable<T[K]>, DynamicTypes>) => void; | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -163,15 +168,14 @@ export type GenerateSetters<T> = { | |||||||||||||||||||||||||||||||||||||||||||||||
| * The final output type of the Generic Binder, providing fully resolved, ready-to-use props. | ||||||||||||||||||||||||||||||||||||||||||||||||
| * This is what framework-specific adapters (like `createReactComponent`) pass to the developer's view logic. | ||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||
| export type ResolveA2uiProps<T> = (T extends object | ||||||||||||||||||||||||||||||||||||||||||||||||
| export type ResolveA2uiProps<T> = T extends object | ||||||||||||||||||||||||||||||||||||||||||||||||
| ? { | ||||||||||||||||||||||||||||||||||||||||||||||||
| [K in keyof T]: ResolveA2uiProp<T[K]>; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| : T) & | ||||||||||||||||||||||||||||||||||||||||||||||||
| GenerateSetters<T> & { | ||||||||||||||||||||||||||||||||||||||||||||||||
| isValid?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||
| validationErrors?: string[]; | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } & GenerateSetters<T> & (T extends {checks: any} ? { | ||||||||||||||||||||||||||||||||||||||||||||||||
| isValid?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||
| validationErrors?: string[]; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } : {}) | ||||||||||||||||||||||||||||||||||||||||||||||||
| : T; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+171
to
+178
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||
| * The Generic Binder is a framework-agnostic engine that transforms raw A2UI JSON payload | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please, test this in antigravity/vscode. I think I tried something similar to this and at some point the typescript inference was giving up attempting to resolve zod stuff (look at
props.somethingin a component implementation, for example)