fix(forms): Support readonly arrays in signal forms

This would allow using `readonly Array<...>` in types
This commit is contained in:
kirjs
2025-12-26 15:17:42 -05:00
committed by Kristiyan Kostadinov
parent 004813dc37
commit 282220d032
3 changed files with 31 additions and 7 deletions
@@ -182,7 +182,7 @@ export interface FieldState<TValue, TKey extends string | number = string | numb
}
// @public
export type FieldTree<TModel, TKey extends string | number = string | number> = (() => [TModel] extends [AbstractControl] ? CompatFieldState<TModel, TKey> : FieldState<TModel, TKey>) & ([TModel] extends [AbstractControl] ? object : [TModel] extends [Array<infer U>] ? ReadonlyArrayLike<MaybeFieldTree<U, number>> : TModel extends Record<string, any> ? Subfields<TModel> : object);
export type FieldTree<TModel, TKey extends string | number = string | number> = (() => [TModel] extends [AbstractControl] ? CompatFieldState<TModel, TKey> : FieldState<TModel, TKey>) & ([TModel] extends [AbstractControl] ? object : [TModel] extends [ReadonlyArray<infer U>] ? ReadonlyArrayLike<MaybeFieldTree<U, number>> : TModel extends Record<string, any> ? Subfields<TModel> : object);
// @public
export type FieldValidator<TValue, TPathKind extends PathKind = PathKind.Root> = LogicFn<TValue, ValidationResult<ValidationError.WithoutField>, TPathKind>;
@@ -513,7 +513,7 @@ export namespace SchemaPathRules {
}
// @public
export type SchemaPathTree<TModel, TPathKind extends PathKind = PathKind.Root> = ([TModel] extends [AbstractControl] ? CompatSchemaPath<TModel, TPathKind> : SchemaPath<TModel, SchemaPathRules.Supported, TPathKind>) & (TModel extends AbstractControl ? unknown : TModel extends Array<any> ? unknown : TModel extends Record<string, any> ? {
export type SchemaPathTree<TModel, TPathKind extends PathKind = PathKind.Root> = ([TModel] extends [AbstractControl] ? CompatSchemaPath<TModel, TPathKind> : SchemaPath<TModel, SchemaPathRules.Supported, TPathKind>) & (TModel extends AbstractControl ? unknown : TModel extends ReadonlyArray<any> ? unknown : TModel extends Record<string, any> ? {
[K in keyof TModel]: MaybeSchemaPathTree<TModel[K], PathKind.Child>;
} : unknown);
+3 -4
View File
@@ -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<TModel, TKey extends string | number = string | number> =
// Children:
([TModel] extends [AbstractControl]
? object
: [TModel] extends [Array<infer U>]
: [TModel] extends [ReadonlyArray<infer U>]
? ReadonlyArrayLike<MaybeFieldTree<U, number>>
: TModel extends Record<string, any>
? Subfields<TModel>
@@ -408,7 +407,7 @@ export type SchemaPathTree<TModel, TPathKind extends PathKind = PathKind.Root> =
(TModel extends AbstractControl
? unknown
: // Array paths have no subpaths
TModel extends Array<any>
TModel extends ReadonlyArray<any>
? unknown
: // Object subfields
TModel extends Record<string, any>
+26 -1
View File
@@ -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<RecursiveType>([5]));
});
it('should allow ReadonlyArray in model and be iterable', () => {
interface Order {
readonly products: readonly string[];
}
const order: WritableSignal<Order> = null!;
const f = form(order);
// Iterating over products should yield FieldTree<string> items, not [string, FieldTree] entries
for (const product of f.products) {
const p: FieldTree<string> = product;
p().value();
}
});
it('should allow Array in model and be iterable', () => {
interface Order {
products: string[];
}
const order: WritableSignal<Order> = null!;
const f = form(order);
for (const product of f.products) {
const p: FieldTree<string> = product;
p().value();
}
});
});
}