From 282220d032a64d32c466bb37057c6f91df39bfb3 Mon Sep 17 00:00:00 2001 From: kirjs Date: Fri, 26 Dec 2025 15:17:42 -0500 Subject: [PATCH] fix(forms): Support readonly arrays in signal forms This would allow using `readonly Array<...>` in types --- goldens/public-api/forms/signals/index.api.md | 4 +-- packages/forms/signals/src/api/types.ts | 7 +++-- .../forms/signals/test/node/types.spec.ts | 27 ++++++++++++++++++- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/goldens/public-api/forms/signals/index.api.md b/goldens/public-api/forms/signals/index.api.md index b323a5558a5..5cbbdd8fb18 100644 --- a/goldens/public-api/forms/signals/index.api.md +++ b/goldens/public-api/forms/signals/index.api.md @@ -182,7 +182,7 @@ export interface FieldState = (() => [TModel] extends [AbstractControl] ? CompatFieldState : FieldState) & ([TModel] extends [AbstractControl] ? object : [TModel] extends [Array] ? ReadonlyArrayLike> : TModel extends Record ? Subfields : object); +export type FieldTree = (() => [TModel] extends [AbstractControl] ? CompatFieldState : FieldState) & ([TModel] extends [AbstractControl] ? object : [TModel] extends [ReadonlyArray] ? ReadonlyArrayLike> : TModel extends Record ? Subfields : object); // @public export type FieldValidator = LogicFn, TPathKind>; @@ -513,7 +513,7 @@ export namespace SchemaPathRules { } // @public -export type SchemaPathTree = ([TModel] extends [AbstractControl] ? CompatSchemaPath : SchemaPath) & (TModel extends AbstractControl ? unknown : TModel extends Array ? unknown : TModel extends Record ? { +export type SchemaPathTree = ([TModel] extends [AbstractControl] ? CompatSchemaPath : SchemaPath) & (TModel extends AbstractControl ? unknown : TModel extends ReadonlyArray ? unknown : TModel extends Record ? { [K in keyof TModel]: MaybeSchemaPathTree; } : unknown); diff --git a/packages/forms/signals/src/api/types.ts b/packages/forms/signals/src/api/types.ts index 08130834b4d..792cdbaaf43 100644 --- a/packages/forms/signals/src/api/types.ts +++ b/packages/forms/signals/src/api/types.ts @@ -9,8 +9,7 @@ import {Signal, ɵFieldState} from '@angular/core'; import {AbstractControl} from '@angular/forms'; import type {Field} from './field_directive'; -import type {MetadataKey} from './rules/metadata'; -import type {ValidationError} from './rules/validation/validation_errors'; +import type {ValidationError, MetadataKey} from './rules'; /** * Symbol used to retain generic type information when it would otherwise be lost. @@ -169,7 +168,7 @@ export type FieldTree = // Children: ([TModel] extends [AbstractControl] ? object - : [TModel] extends [Array] + : [TModel] extends [ReadonlyArray] ? ReadonlyArrayLike> : TModel extends Record ? Subfields @@ -408,7 +407,7 @@ export type SchemaPathTree = (TModel extends AbstractControl ? unknown : // Array paths have no subpaths - TModel extends Array + TModel extends ReadonlyArray ? unknown : // Object subfields TModel extends Record diff --git a/packages/forms/signals/test/node/types.spec.ts b/packages/forms/signals/test/node/types.spec.ts index 57733854a12..b6121fdf0b9 100644 --- a/packages/forms/signals/test/node/types.spec.ts +++ b/packages/forms/signals/test/node/types.spec.ts @@ -7,7 +7,7 @@ */ import {signal, WritableSignal} from '@angular/core'; -import {form, required, schema, SchemaFn} from '../../public_api'; +import {FieldTree, form, required, schema, SchemaFn} from '../../public_api'; interface Order { id: string; @@ -87,5 +87,30 @@ function typeVerificationOnlyDoNotRunMe() { type RecursiveType = (number | RecursiveType)[]; form(signal([5])); }); + + it('should allow ReadonlyArray in model and be iterable', () => { + interface Order { + readonly products: readonly string[]; + } + const order: WritableSignal = null!; + const f = form(order); + // Iterating over products should yield FieldTree items, not [string, FieldTree] entries + for (const product of f.products) { + const p: FieldTree = product; + p().value(); + } + }); + + it('should allow Array in model and be iterable', () => { + interface Order { + products: string[]; + } + const order: WritableSignal = null!; + const f = form(order); + for (const product of f.products) { + const p: FieldTree = product; + p().value(); + } + }); }); }