Skip to content

Commit f18e173

Browse files
dylhunnalxhub
authored andcommitted
fix(forms): allow FormBuilder.group(...) to accept optional fields. (#46253)
Consider the case in which `FormBuilder` is used to construct a group with an optional field: ``` const controls = { name: fb.control('') }; const foo: FormGroup<{ name: FormControl<string | null>; address?: FormControl<string | null>; }> = fb.group<{ name: FormControl<string | null>; address?: FormControl<string | null>; }>(controls); ``` Today, with fully strict TypeScript settings, the above will not compile: ``` Types of property 'controls' are incompatible. Type '{ name: FormControl<string | null>; address?: FormControl<FormGroup<SubFormControls> | null | undefined> | undefined; }' is not assignable to type '{ name: FormControl<string | null>; address?: FormGroup<SubFormControls> | undefined; }'. ``` Notice that the `fb.group(...)` is calculating the following type for address: `address?: FormControl<FormGroup<string|null>`. This is clearly wrong -- an extraneous `FormControl` has been added! This is coming from the calculation of the result type of `fb.group(...)`. In the type definition, if we cannot detect the outer control type, [we assume it's just an unwrapped value, and automatically wrap it in `FormControl`](https://github.com/angular/angular/blob/14.0.0/packages/forms/src/form_builder.ts#L66). Because the optional `{address?: FormControl<string|null>}` implicitly makes the RHS have type `FormControl<string|null>|undefined`, [the relevant condition is not satisfied](https://github.com/angular/angular/blob/14.0.0/packages/forms/src/form_builder.ts#L55). In particular, the condition expects just `FormGroup<T>`, not `FormGroup<T>|undefined`. So we assume `T` is a value type, and it gets wrapped with `FormControl`. The solution is to add the cases where `undefined` is included in the union type when detecting which control `T` is (if any). PR Close #46253
1 parent 0d10fe5 commit f18e173

File tree

4 files changed

+31
-8
lines changed

4 files changed

+31
-8
lines changed

packages/core/test/bundling/forms_reactive/bundle.golden_symbols.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -548,9 +548,6 @@
548548
{
549549
"name": "_randomChar"
550550
},
551-
{
552-
"name": "componentDefCount"
553-
},
554551
{
555552
"name": "_symbolIterator"
556553
},
@@ -656,6 +653,9 @@
656653
{
657654
"name": "collectStylingFromTAttrs"
658655
},
656+
{
657+
"name": "componentDefCount"
658+
},
659659
{
660660
"name": "compose"
661661
},
@@ -1595,4 +1595,4 @@
15951595
{
15961596
"name": "ɵɵtext"
15971597
}
1598-
]
1598+
]

packages/core/test/bundling/forms_template_driven/bundle.golden_symbols.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -539,9 +539,6 @@
539539
{
540540
"name": "_randomChar"
541541
},
542-
{
543-
"name": "componentDefCount"
544-
},
545542
{
546543
"name": "_symbolIterator"
547544
},
@@ -638,6 +635,9 @@
638635
{
639636
"name": "collectStylingFromTAttrs"
640637
},
638+
{
639+
"name": "componentDefCount"
640+
},
641641
{
642642
"name": "composeAsyncValidators"
643643
},
@@ -1583,4 +1583,4 @@
15831583
{
15841584
"name": "ɵɵtext"
15851585
}
1586-
]
1586+
]

packages/forms/src/form_builder.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,26 @@ export type ɵElement<T, N extends null> =
5252
// The `extends` checks are wrapped in arrays in order to prevent TypeScript from applying type unions
5353
// through the distributive conditional type. This is the officially recommended solution:
5454
// https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types
55+
//
56+
// Identify FormControl container types.
5557
[T] extends [FormControl<infer U>] ? FormControl<U> :
58+
// Or FormControl containers that are optional in their parent group.
59+
[T] extends [FormControl<infer U>|undefined] ? FormControl<U> :
60+
// FormGroup containers.
5661
[T] extends [FormGroup<infer U>] ? FormGroup<U> :
62+
// Optional FormGroup containers.
63+
[T] extends [FormGroup<infer U>|undefined] ? FormGroup<U> :
64+
// FormArray containers.
5765
[T] extends [FormArray<infer U>] ? FormArray<U> :
66+
// Optional FormArray containers.
67+
[T] extends [FormArray<infer U>|undefined] ? FormArray<U> :
68+
// Otherwise unknown AbstractControl containers.
5869
[T] extends [AbstractControl<infer U>] ? AbstractControl<U> :
70+
// Optional AbstractControl containers.
71+
[T] extends [AbstractControl<infer U>|undefined] ? AbstractControl<U> :
72+
// FormControlState object container, which produces a nullable control.
5973
[T] extends [FormControlState<infer U>] ? FormControl<U|N> :
74+
// A ControlConfig tuple, which produces a nullable control.
6075
[T] extends [ControlConfig<infer U>] ? FormControl<U|N> :
6176
// ControlConfig can be too much for the compiler to infer in the wrapped case. This is
6277
// not surprising, since it's practically death-by-polymorphism (e.g. the optional validators

packages/forms/test/typed_integration_spec.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,6 +1032,14 @@ describe('Typed Class', () => {
10321032
}
10331033
});
10341034

1035+
it('from objects with optional keys', () => {
1036+
const controls = {name: fb.control('')};
1037+
const foo:
1038+
FormGroup<{name: FormControl<string|null>; address?: FormControl<string|null>;}> =
1039+
fb.group<{name: FormControl<string|null>; address?: FormControl<string|null>;}>(
1040+
controls);
1041+
});
1042+
10351043
it('from objects with FormControlState', () => {
10361044
const c = fb.group({foo: {value: 'bar', disabled: false}});
10371045
{

0 commit comments

Comments
 (0)