Skip to content

Commit b302797

Browse files
dylhunnPawel Kozlowski
authored andcommitted
fix(forms): Correctly infer FormBuilder types involving [value, validators] shorthand in more cases. (#47034)
Type inference in cases involving `ControlConfig` was previously not working as desired. This was because the compiler was enforcing that `ControlConfig` is a *tuple* -- which is not always that easy to prove! By relaxing this constraint a bit, and just inferring from `ControlConfig` as an array, the type inference catches many more cases, and is generally more correct. PR Close #47034
1 parent dc52cef commit b302797

File tree

2 files changed

+62
-7
lines changed

2 files changed

+62
-7
lines changed

packages/forms/src/form_builder.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,21 @@ function isAbstractControlOptions(options: AbstractControlOptions|{[key: string]
2323
(options as AbstractControlOptions).updateOn !== undefined);
2424
}
2525

26+
/**
27+
* The union of all validator types that can be accepted by a ControlConfig.
28+
*/
29+
type ValidatorConfig = ValidatorFn|AsyncValidatorFn|ValidatorFn[]|AsyncValidatorFn[];
30+
31+
/**
32+
* The compiler may not always be able to prove that the elements of the control config are a tuple
33+
* (i.e. occur in a fixed order). This slightly looser type is used for inference, to catch cases
34+
* where the compiler cannot prove order and position.
35+
*
36+
* For example, consider the simple case `fb.group({foo: ['bar', Validators.required]})`. The
37+
* compiler will infer this as an array, not as a tuple.
38+
*/
39+
type PermissiveControlConfig<T> = Array<T|FormControlState<T>|ValidatorConfig>;
40+
2641
/**
2742
* ControlConfig<T> is a tuple containing a value of type T, plus optional validators and async
2843
* validators.
@@ -31,7 +46,6 @@ function isAbstractControlOptions(options: AbstractControlOptions|{[key: string]
3146
*/
3247
export type ControlConfig<T> = [T|FormControlState<T>, (ValidatorFn|(ValidatorFn[]))?, (AsyncValidatorFn|AsyncValidatorFn[])?];
3348

34-
3549
// Disable clang-format to produce clearer formatting for this multiline type.
3650
// clang-format off
3751

@@ -68,12 +82,7 @@ export type ɵElement<T, N extends null> =
6882
// FormControlState object container, which produces a nullable control.
6983
[T] extends [FormControlState<infer U>] ? FormControl<U|N> :
7084
// A ControlConfig tuple, which produces a nullable control.
71-
[T] extends [ControlConfig<infer U>] ? FormControl<U|N> :
72-
// ControlConfig can be too much for the compiler to infer in the wrapped case. This is
73-
// not surprising, since it's practically death-by-polymorphism (e.g. the optional validators
74-
// members that might be arrays). Watch for ControlConfigs that might fall through.
75-
[T] extends [Array<infer U|ValidatorFn|ValidatorFn[]|AsyncValidatorFn|AsyncValidatorFn[]>] ? FormControl<U|N> :
76-
// Fallthough case: T is not a container type; use it directly as a value.
85+
[T] extends [PermissiveControlConfig<infer U>] ? FormControl<Exclude<U, ValidatorConfig>|N> :
7786
FormControl<T|N>;
7887

7988
// clang-format on

packages/forms/test/typed_integration_spec.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,6 +1019,16 @@ describe('Typed Class', () => {
10191019
t1 = null as unknown as RawValueType;
10201020
}
10211021
});
1022+
1023+
it('with array values', () => {
1024+
const c = fb.control([1, 2, 3]);
1025+
{
1026+
type RawValueType = number[]|null;
1027+
let t: RawValueType = c.getRawValue();
1028+
let t1 = c.getRawValue();
1029+
t1 = null as unknown as RawValueType;
1030+
}
1031+
});
10221032
});
10231033

10241034
describe('should build FormGroups', () => {
@@ -1060,6 +1070,16 @@ describe('Typed Class', () => {
10601070
}
10611071
});
10621072

1073+
it('from objects with FormControlStates nested inside ControlConfigs', () => {
1074+
const c = fb.group({foo: [{value: 'bar', disabled: true}, Validators.required]});
1075+
{
1076+
type ControlsType = {foo: FormControl<string|null>};
1077+
let t: ControlsType = c.controls;
1078+
let t1 = c.controls;
1079+
t1 = null as unknown as ControlsType;
1080+
}
1081+
});
1082+
10631083
it('from objects with ControlConfigs and validators', () => {
10641084
const c = fb.group({foo: ['bar', Validators.required]});
10651085
{
@@ -1068,6 +1088,23 @@ describe('Typed Class', () => {
10681088
let t1 = c.controls;
10691089
t1 = null as unknown as ControlsType;
10701090
}
1091+
1092+
const c2 = fb.group({foo: [[1, 2, 3], Validators.required]});
1093+
{
1094+
type ControlsType = {foo: FormControl<number[]|null>};
1095+
let t: ControlsType = c2.controls;
1096+
let t1 = c2.controls;
1097+
t1 = null as unknown as ControlsType;
1098+
}
1099+
expect(c2.controls.foo.value).toEqual([1, 2, 3]);
1100+
1101+
const c3 = fb.group({foo: [null, Validators.required]});
1102+
{
1103+
type ControlsType = {foo: FormControl<null>};
1104+
let t: ControlsType = c3.controls;
1105+
let t1 = c3.controls;
1106+
t1 = null as unknown as ControlsType;
1107+
}
10711108
});
10721109

10731110
it('from objects with ControlConfigs and validator lists', () => {
@@ -1482,6 +1519,15 @@ describe('Typed Class', () => {
14821519
}
14831520
c.reset();
14841521
expect(c.value).toEqual({foo: 'bar'});
1522+
1523+
const c2 = fb.group({foo: [[1, 2, 3], Validators.required]});
1524+
{
1525+
type ControlsType = {foo: FormControl<number[]>};
1526+
let t: ControlsType = c2.controls;
1527+
let t1 = c2.controls;
1528+
t1 = null as unknown as ControlsType;
1529+
}
1530+
expect(c2.controls.foo.value).toEqual([1, 2, 3]);
14851531
});
14861532

14871533
it('from objects with ControlConfigs and validator lists', () => {

0 commit comments

Comments
 (0)