Skip to content

Preserve per-field validator and metadata typing in Signal Forms for better composition and introspection #69321

@ronnain

Description

@ronnain

Which @angular/* package(s) are relevant/related to the feature request?

No response

Description

I would like to propose an improvement for Signal Forms typing.

Today, Signal Forms lose important type information after rules are registered:

We cannot statically know which validators were attached to each field.
We cannot statically know which metadata entries were attached to each field (and their value types).
This limits advanced composition patterns, especially extracting complex form logic into local sub-components while preserving field-level typing.
This makes some advanced UX and architecture use cases hard to express:

Strongly-typed first failed validator APIs for templates and control-flow.
Type-safe metadata consumption at field level.
Type-safe extraction of complex field groups and list item logic into sub-parts of the form.

Proposed solution

(illustrative, not strict syntax)

loginForm = form(this.loginModel, function* (schemaPath) {
  yield* required(schemaPath.email, {message: 'Email is required'});
  yield* email(schemaPath.email, {message: 'Enter a valid email address'});
  yield* required(schemaPath.password, {message: 'Password is required'});
});

With this style, the resulting form type could preserve:

  • email has validators: required | email, potentially with order information.
  • password has validator: required.

It would not be necessary to restrict the error to { message: '...' }; the user could return whatever they want, since it is preserved at the type level.

Another example with collections:

invoiceForm = form(this.invoiceModel, function* (schemaPath) {
  yield* required(schemaPath.invoiceNumber);
  yield* applyEach(schemaPath.lineItems, function* (item) {
    yield* required(item.name);
    yield* min(item.quantity, 1);
  });
});

This could preserve typed validator info per list item field as well.

Same idea for metadata:

registrationForm = form(this.registrationModel, function* (path) {
  yield* metadata(path.username, 'USERNAME_HELP', ({value}) => {
    const username = value();
    if (username.length === 0) return 'Choose a unique username between 3 and 20 characters.' as const;
    if (username.length < 3) return 'Keep typing, usernames are at least 3 characters.' as const;;
    if (username.length > 20) return 'Usernames are at most 20 characters.' as const;
    return 'Looks good.';
  });
});

usernameHelp = computed(() => this.registrationForm.username().metadata.USERNAME_HELP?.() ?? ''); // metadata with safe access to USERNAME_HELP, 'Usernames are at most 20 characters.' | 'Keep typing, usernames are at least 3 characters.' | 'Choose a unique username between 3 and 20 characters.'

This would allow username metadata to remain typed (string in this example), and potentially reduce key boilerplate depending on final design.

Potential ergonomic outcome

  • Access typed validator information from field APIs, including declaration order.
  • Enable a typed firstFailedValidator-style API returning the union of validators attached to the field.
  • Improve template control-flow patterns, for example switch-style rendering by validator kind.
  • Improve extraction of complex form segments into sub-components without losing typing.

Important note

I am not attached to generator syntax specifically. The core request is preserving richer per-field type information (validators, metadata, and possibly order) through form construction.

Why this matters

  • Better IDE guidance and safer refactoring.
  • Better template ergonomics for advanced validation UX.
  • Better composability for complex forms.
  • Stronger type guarantees for teams building domain-heavy form experiences.

If this direction is interesting, I can go further and provide additional proposals focused on Signal Forms typing improvements. I already find this approach simple to implement, with clear practical benefits.

Also, it should be straightforward to produce a quick POC with AI assistance, as current AI tools handle these typing and API-shape concepts quite well.

Thanks for considering this.

Alternatives considered

My lib 😅: https://ng-angular-stack.github.io/craft/forms/

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: formsgemini-triagedLabel noting that an issue has been triaged by gemini

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions