FormValueContract
FormValueContract<TValue> is a compile-time contract for checking the aggregate committed value
of a form() or group(). Use it with TypeScript's satisfies operator so the definitions remain
the source of every concrete child-node type.
type FormValueContract<TValue extends object> = {
(): TValue;
value: Signal<TValue>;
};
Import it from the package entry point:
import { type FormValueContract } from '@ngblocks/form-nodes';
📝 Check a form value
import { field, form, type FormValueContract } from '@ngblocks/form-nodes';
type Profile = {
username: string | null;
age: number | null;
country: string;
};
const profile = form({
username: field(''),
age: field(0),
country: field.strict<string>('Switzerland'),
}) satisfies FormValueContract<Profile>;
const value: Profile = profile(); // { username: '', age: 0, country: 'Switzerland' }
void value;
The contract verifies both ordinary value-reading surfaces:
profile(); // { username: '', age: 0, country: 'Switzerland' }
profile.value(); // { username: '', age: 0, country: 'Switzerland' }
The expression retains the type inferred by form(). username and age remain their concrete
FieldNode nodes, and country remains a FieldNode<string> declared with field.strict<string>().
The Profile model verifies their aggregate value.
📝 Incompatible values
An incompatible child makes the satisfies expression fail:
type Profile = {
username: string | null;
age: number | null;
};
form({
username: field(42),
age: field(0),
}) satisfies FormValueContract<Profile>;
// TypeScript error: `number | null` is not assignable to `string | null`.
🌳 Use with group()
The contract is structural and also accepts a group:
type Address = {
city: string | null;
postcode: number | null;
};
const address = group({
city: field(''),
postcode: field(0),
}) satisfies FormValueContract<Address>;
address(); // { city: '', postcode: 0 }
address.city(); // ''
💡 Why satisfies matters
Avoid annotating the variable as the contract:
const profile: FormValueContract<Profile> = form({
username: field(''),
age: field(0),
});
That annotation replaces the expression's visible type with FormValueContract<Profile>, hiding
the inferred children. satisfies performs the compatibility check and returns the original form
type unchanged.
Likewise, FormValueContract is separate from the first generic currently inferred by form().
That generic describes the definitions so TypeScript can preserve each concrete node. The contract
lets a named value model provide an additional check without replacing that definition inference.
📚 Arrays and nullability
After the basic object contract, the same pattern can validate dynamic arrays while preserving
their concrete ArrayNode API:
import { array, field, form, type FormValueContract } from '@ngblocks/form-nodes';
type Profile = {
username: string | null;
items: (string | null)[];
};
const profile = form({
username: field(''),
items: array(field('')),
}) satisfies FormValueContract<Profile>;
const value: Profile = profile();
profile.items.push('Angular');
void value;
In this example, items remains an ArrayNode with push(), at(), iteration, and the other
structural operations. The Profile model validates the array value but does not select the
primitive.
Field nullability is part of the comparison. A default field includes null, while an array node
does not make its complete array value nullable. For an atomic array field, put null around the
complete array value:
type AtomicItems = {
items: string[] | null;
};
const atomicItems = form({
items: field<string[]>(),
}) satisfies FormValueContract<AtomicItems>;
atomicItems.items(); // null
This differs from an array node whose individual item fields are nullable:
type DynamicItems = {
items: (string | null)[];
};
const dynamicItems = form({
items: array(field('')),
}) satisfies FormValueContract<DynamicItems>;
🌳 Structural compatibility
The check follows TypeScript's normal structural assignability. A form value must provide every
required property in TValue with a compatible type. A form may contain additional properties and
still satisfy a narrower model because structural TypeScript types permit extra properties in an
already inferred value.
FormValueContract has no runtime representation, performs no validation after compilation, and
does not affect values, state propagation, validators, or control bindings.
🔗 Related reference
FormNodeValueextracts a value model from an existing form instance.form()creates a submission workflow and infers its complete child tree.group()creates an object aggregate without an independent submission workflow.array()creates the dynamic collection preserved by the contract example.- Creating nodes explains definition inference and primitive choice.