Skip to content

fix(forms): support generic unions in signal form schemas#67304

Merged
mattrbeck merged 1 commit intoangular:mainfrom
alxhub:sf-fix-generics
Mar 17, 2026
Merged

fix(forms): support generic unions in signal form schemas#67304
mattrbeck merged 1 commit intoangular:mainfrom
alxhub:sf-fix-generics

Conversation

@alxhub
Copy link
Copy Markdown
Member

@alxhub alxhub commented Feb 25, 2026

This commit resolves an issue where using an uninstantiated generic type parameter in a signal form model caused TypeScript compilation failures due to distributive conditional types (#66596). The previous attempt to fix this issue by tuple-wrapping everything caused another bug (#65535) that prevented property access on generic unions.

This commit balances the need to resolve nested generic property access while handling infinitely recursive generic structures without depth errors.

What changed and why:

  • Base State Wrappers: Tuple wrappers ([TModel] extends [AbstractControl]) are applied to FieldTreeBase to safely defer generic evaluation. This prevents primitive unions (like boolean) from incorrectly evaluating to never.
  • Naked Map Over Children: Object subfield checks (TModel extends Record) are re-evaluated as purely naked conditionals. Eager distribution over generics allows users to directly access shared properties of unresolved union types.
  • Array Interface Deflection: ReadonlyArrayLike<T> generic abstraction is redefined as an explicit interface instead of a mapped Pick type alias. This optimally intercepts TypeScript from eagerly evaluating infinitely recursive array structures (e.g. RecursiveType = (number | RecursiveType)[]).
  • Overloaded Context Methods: FieldNodeContext.stateOf and fieldTreeOf are defined as explicitly overloaded class methods and lexically bound (this) in the constructor. These changes are required to safely align the runtime bindings with the tautological conditionals implemented in the RootFieldContext interface structure.

Fixes #65535

@ngbot ngbot bot added this to the Backlog milestone Feb 25, 2026
@pullapprove pullapprove bot requested review from atscott and kirjs February 25, 2026 22:56
@pullapprove pullapprove bot requested a review from kirjs February 27, 2026 00:26
Copy link
Copy Markdown
Contributor

@kirjs kirjs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed-for: public-api

This commit resolves an issue where using an uninstantiated generic type
parameter in a signal form model caused TypeScript compilation failures due to
distributive conditional types (angular#66596). The previous attempt to fix this issue
by tuple-wrapping everything caused another bug (angular#65535) that prevented property
access on generic unions.

This commit balances the need to resolve nested generic property access while
handling infinitely recursive generic structures without depth errors.

What changed and why:
- Base State Wrappers: Tuple wrappers (`[TModel] extends [AbstractControl]`) are
  applied to `FieldTreeBase` to safely defer generic evaluation. This prevents
  primitive unions (like `boolean`) from incorrectly evaluating to `never`.
- Naked Map Over Children: Object subfield checks (`TModel extends Record`) are
  re-evaluated as purely naked conditionals. Eager distribution over generics
  allows users to directly access shared properties of unresolved union types.
- Array Interface Deflection: `ReadonlyArrayLike<T>` generic abstraction is
  redefined as an explicit `interface` instead of a mapped `Pick` type alias.
  This optimally intercepts TypeScript from eagerly evaluating infinitely
  recursive array structures (e.g. `RecursiveType = (number | RecursiveType)[]`).
- Overloaded Context Methods: `FieldNodeContext.stateOf` and `fieldTreeOf` are
  defined as explicitly overloaded class methods and lexically bound (`this`) in
  the constructor. These changes are required to safely align the runtime bindings
  with the tautological conditionals implemented in the `RootFieldContext`
  interface structure.

Fixes angular#65535
@alxhub alxhub added target: major This PR is targeted for the next major release action: merge The PR is ready for merge by the caretaker merge: caretaker note Alert the caretaker performing the merge to check the PR for an out of normal action needed or note labels Mar 17, 2026
@alxhub
Copy link
Copy Markdown
Member Author

alxhub commented Mar 17, 2026

Caretaker: another "green" presubmit

@mattrbeck mattrbeck merged commit 83032e3 into angular:main Mar 17, 2026
23 of 25 checks passed
@mattrbeck
Copy link
Copy Markdown
Member

This PR was merged into the repository. The changes were merged into the following branches:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

action: merge The PR is ready for merge by the caretaker area: forms forms: signals merge: caretaker note Alert the caretaker performing the merge to check the PR for an out of normal action needed or note target: major This PR is targeted for the next major release

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Signal forms: Generics don't work the same as regular objects

3 participants