a2ui-project / a2ui-project/a2ui

[BUG]: GenericBinder generates an uncallable setter for a binding-only property

Abierto Apto para principiantes
#2,528 1 comentario 0 reacciones 1 asignado Reclamado por @andrewkolos Ver en GitHub
status: first-line-handled
Lenguaje dominante
TypeScript
Estrellas
16.4k
Forks
1.3k
Merge medio
2 d 13 h
PR fusionados (30 d)
134

Descripción

- [x] I have searched the existing issues to make sure this bug has not already been reported.

## Describe the Bug

### Background

`@a2ui/web_core`'s `GenericBinder` (`renderers/web_core/src/v0_9/rendering/generic-binder.ts`) is
the engine that turns a component's raw A2UI JSON config plus its Zod schema into the resolved,
strongly typed props a renderer hands to a component implementation. The React and Lit renderers
both build their component props on it (Angular does not, since it rolls its own binding).

Two exported utility types in that file describe the result:

- `ResolveA2uiProp` types what a component **reads**: an `Action` becomes `() => void`, a
`DynamicString` becomes `string`, and so on.
- `GenerateSetters` types what a component **writes**: for a `value: DynamicString` property
it generates a `setValue(v: string)` method for two-way binding.

Both start from the same idea. A dynamic property is declared as a union of literal branches plus
the binding shapes, for example `DynamicString = string | DataBinding | FunctionCall`. To find the
resolved type, both subtract the binding shapes: `Exclude`.

### The bug

A property can be declared with **only** binding branches and no literal one, for example
`z.union([DataBindingSchema, FunctionCallSchema])`. For such a property the subtraction removes
everything and leaves nothing, and the two types disagree about what to do next.

`ResolveA2uiProp` guards that case and falls back to `any` (lines 175-177):

```ts
: Exclude extends never
? any
: Exclude;
```

`GenerateSetters` has no equivalent guard (same file, line 185):

```ts
value: Exclude, DynamicTypes>,
```

So for a property declared as `DataBinding | FunctionCall` the read side falls back to `any`,
while the write side resolves to `(value: never) => void`. `never` is uninhabited, so no value
can be passed and the generated setter cannot be called at all. The asymmetry is the whole
defect: the read path has a fallback and the write path was never given one.

No component in the basic catalog declares such a property, and `DataBinding` is absent from
`COMMON_TYPE_SCHEMAS` in `src/v0_9/catalog/schema_loader.ts`, so JSON-defined catalogs cannot
declare one either. The case is reachable from hand-written Zod component schemas, which is
presumably why it has gone unnoticed.

## Steps to Reproduce

1. Declare a component whose schema has a property with only binding branches.
2. Read the generated setter's parameter type.

```ts
import {z} from 'zod';
import {DataBindingSchema, FunctionCallSchema} from '@a2ui/web_core/v0_9';
import type {ResolveA2uiProps} from '@a2ui/web_core/v0_9';

const BindingOnly = z.object({sort: z.union([DataBindingSchema, FunctionCallSchema])});
declare const props: ResolveA2uiProps>;

props.sort; // any - the read-side fallback applies
props.setSort; // (value: never) => void - uncallable

props.setSort({path: '/sortOrder'});
```

3. Run `tsc --noEmit`. The call on the last line is rejected.

## Expected Behavior

The generated setter is callable. The read side already decides what to do when the subtraction
leaves nothing; the write side should make the same decision rather than producing a parameter
no value can satisfy.

## Screenshots / Video / Logs

```
error TS2345: Argument of type '{ path: string; }' is not assignable to parameter of type 'never'.
```

## Environment Details

- **OS**: macOS 15
- **Browser/Platform**: Node.js, TypeScript 5.9.3 (compile-time only, no runtime behaviour involved)
- **SDK/Package Name & Version**: `@a2ui/web_core` 0.10.7 (reproduced against `main` @ `0dc4a30f`)
- **Protocol Version**: v0.9, v0.9.1
- **Agent Framework & LLM Model**: n/a

## Additional Context

**Suggested fix** - give the write side the fallback the read side already has:

```ts
value: [Exclude, DynamicTypes>] extends [never]
? unknown
: Exclude, DynamicTypes>,
```

`unknown` rather than `any`: it accepts every argument while keeping the value checked at the
call site, and does not add to the `any` count tracked in #1296.

This is non-breaking. A parameter of type `never` admits no argument today, so widening it can
only permit calls that previously failed to compile; no existing call site can change meaning.

Guía de contribución

Abrir la guía de contribución

Línea de trabajo

The bug is in renderers/web_core/src/v0_9/rendering/generic-binder.ts, specifically the GenerateSetters type. Compare it with ResolveA2uiProp to see the missing fallback for binding-only properties. Write a test in the same directory to verify the fix: a property declared with only DataBinding and FunctionCall branches should generate a callable setter. Run TypeScript compilation to confirm the setter's parameter type is no longer 'never'.

Escrito por el modelo de indexación a partir del texto del issue.

Evaluación

Stack tecnológico
typescript
Área
backend-api-design, tooling
Tipo de issue
Error
Dificultad
2/5
Tiempo estimado
1-3 horas
Estado de actividad
Activo
Claridad
Bien especificado
Aptitud para principiantes
65/100

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.