Skip to content

Commit ff3f5a8

Browse files
committed
fix(forms): Fix a typing bug in FormBuilder. (#45684)
Previously, the following code would fail to compile: ``` let form: FormGroup<{email: FormControl<string | null>}>; form = fb.group({ email: ['', Validators.required] }); ``` This is because the compiler was unable to properly infer the inner type of `ControlConfig` arrays in some cases. The same issue applies to `FormArray` as well under certain circumstances. This change cleans up the `FormBuilder` type signatures to always use the explicit Element type, and to catch `ControlConfig` types that might fall through. PR Close #45684
1 parent 788f587 commit ff3f5a8

File tree

4 files changed

+78
-64
lines changed

4 files changed

+78
-64
lines changed

goldens/public-api/forms/index.md

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -276,29 +276,16 @@ export class FormArrayName extends ControlContainer implements OnInit, OnDestroy
276276

277277
// @public
278278
export class FormBuilder {
279-
// (undocumented)
280-
array<T>(controls: Array<FormControl<T>>, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormArray<FormControl<T>>;
281-
// (undocumented)
282-
array<T extends {
283-
[K in keyof T]: AbstractControl<any>;
284-
}>(controls: Array<FormGroup<T>>, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormArray<FormGroup<T>>;
285-
// (undocumented)
286-
array<T extends AbstractControl<any>>(controls: Array<FormArray<T>>, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormArray<FormArray<T>>;
287-
// (undocumented)
288-
array<T extends AbstractControl<any>>(controls: Array<AbstractControl<T>>, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormArray<AbstractControl<T>>;
289-
// (undocumented)
290-
array<T>(controls: Array<FormControlState<T> | ControlConfig<T> | T>, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormArray<FormControl<T | null>>;
279+
array<T>(controls: Array<T>, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormArrayElement<T>>;
291280
// (undocumented)
292281
control<T>(formState: T | FormControlState<T>, opts: FormControlOptions & {
293282
initialValueIsDefault: true;
294283
}): FormControl<T>;
295284
// (undocumented)
296285
control<T>(formState: T | FormControlState<T>, validatorOrOpts?: ValidatorFn | ValidatorFn[] | FormControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormControl<T | null>;
297286
// (undocumented)
298-
group<T extends {
299-
[K in keyof T]: FormControlState<any> | ControlConfig<any> | FormControl<any> | FormGroup<any> | FormArray<any> | AbstractControl<any> | T[K];
300-
}>(controls: T, options?: AbstractControlOptions | null): FormGroup<{
301-
[K in keyof T]: ɵGroupElement<T[K]>;
287+
group<T extends {}>(controls: T, options?: AbstractControlOptions | null): FormGroup<{
288+
[K in keyof T]: ɵElement<T[K]>;
302289
}>;
303290
// @deprecated
304291
group(controls: {

packages/forms/src/form_builder.ts

Lines changed: 42 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,19 @@ import {FormArray, UntypedFormArray} from './model/form_array';
1515
import {FormControl, FormControlOptions, FormControlState, UntypedFormControl} from './model/form_control';
1616
import {FormGroup, UntypedFormGroup} from './model/form_group';
1717

18-
function isAbstractControlOptions(options: AbstractControlOptions|
19-
{[key: string]: any}): options is AbstractControlOptions {
20-
return (<AbstractControlOptions>options).asyncValidators !== undefined ||
21-
(<AbstractControlOptions>options).validators !== undefined ||
22-
(<AbstractControlOptions>options).updateOn !== undefined;
18+
function isAbstractControlOptions(options: AbstractControlOptions|{[key: string]: any}|null|
19+
undefined): options is AbstractControlOptions {
20+
return !!options &&
21+
((options as AbstractControlOptions).asyncValidators !== undefined ||
22+
(options as AbstractControlOptions).validators !== undefined ||
23+
(options as AbstractControlOptions).updateOn !== undefined);
24+
}
25+
26+
function isFormControlOptions(options: FormControlOptions|{[key: string]: any}|null|
27+
undefined): options is FormControlOptions {
28+
return !!options &&
29+
(isAbstractControlOptions(options) ||
30+
(options as FormControlOptions).initialValueIsDefault !== undefined);
2331
}
2432

2533
/**
@@ -30,15 +38,25 @@ function isAbstractControlOptions(options: AbstractControlOptions|
3038
*/
3139
export type ControlConfig<T> = [T|FormControlState<T>, (ValidatorFn|(ValidatorFn[]))?, (AsyncValidatorFn|AsyncValidatorFn[])?];
3240

33-
// Disable clang-format to produce clearer formatting for this multiline type.
41+
// Disable clang-format to produce clearer formatting for these multiline types.
3442
// clang-format off
35-
export type ɵGroupElement<T> =
43+
44+
/**
45+
* FormBuilder accepts values in various container shapes, as well as raw values.
46+
* Element returns the appropriate corresponding model class.
47+
*/
48+
export type ɵElement<T> =
3649
T extends FormControl<infer U> ? FormControl<U> :
3750
T extends FormGroup<infer U> ? FormGroup<U> :
3851
T extends FormArray<infer U> ? FormArray<U> :
3952
T extends AbstractControl<infer U> ? AbstractControl<U> :
4053
T extends FormControlState<infer U> ? FormControl<U|null> :
4154
T extends ControlConfig<infer U> ? FormControl<U|null> :
55+
// ControlConfig can be too much for the compiler to infer in the wrapped case. This is
56+
// not surprising, since it's practically death-by-polymorphism (e.g. the optional validators
57+
// members that might be arrays). Watch for ControlConfigs that might fall through.
58+
T extends Array<infer U|ValidatorFn|ValidatorFn[]|AsyncValidatorFn|AsyncValidatorFn[]> ? FormControl<U|null> :
59+
// Fallthough case: T is not a container type; use is directly as a value.
4260
FormControl<T|null>;
4361

4462
// clang-format on
@@ -57,13 +75,10 @@ export type ɵGroupElement<T> =
5775
*/
5876
@Injectable({providedIn: ReactiveFormsModule})
5977
export class FormBuilder {
60-
group<T extends {
61-
[K in keyof T]: FormControlState<any>| ControlConfig<any>| FormControl<any>| FormGroup<any>|
62-
FormArray<any>| AbstractControl<any>| T[K]
63-
}>(
78+
group<T extends {}>(
6479
controls: T,
6580
options?: AbstractControlOptions|null,
66-
): FormGroup<{[K in keyof T]: ɵGroupElement<T[K]>}>;
81+
): FormGroup<{[K in keyof T]: ɵElement<T[K]>}>;
6782

6883
/**
6984
* @description
@@ -124,12 +139,14 @@ export class FormBuilder {
124139
updateOn = options.updateOn != null ? options.updateOn : undefined;
125140
} else {
126141
// `options` are legacy form group options
127-
validators = options['validator'] != null ? options['validator'] : null;
128-
asyncValidators = options['asyncValidator'] != null ? options['asyncValidator'] : null;
142+
validators = (options as any)['validator'] != null ? (options as any)['validator'] : null;
143+
asyncValidators =
144+
(options as any)['asyncValidator'] != null ? (options as any)['asyncValidator'] : null;
129145
}
130146
}
131147

132-
return new FormGroup(reducedControls, {asyncValidators, updateOn, validators});
148+
// Cast to `any` because the inferred types are not as specific as Element.
149+
return new FormGroup(reducedControls, {asyncValidators, updateOn, validators}) as any;
133150
}
134151

135152
control<T>(formState: T|FormControlState<T>, opts: FormControlOptions&{
@@ -169,36 +186,10 @@ export class FormBuilder {
169186
control<T>(
170187
formState: T|FormControlState<T>,
171188
validatorOrOpts?: ValidatorFn|ValidatorFn[]|FormControlOptions|null,
172-
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|
173-
null): FormControl<T|null>|FormControl<T> {
189+
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormControl {
174190
return new FormControl(formState, validatorOrOpts, asyncValidator);
175191
}
176192

177-
array<T>(
178-
controls: Array<FormControl<T>>,
179-
validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null,
180-
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormArray<FormControl<T>>;
181-
182-
array<T extends {[K in keyof T]: AbstractControl<any>}>(
183-
controls: Array<FormGroup<T>>,
184-
validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null,
185-
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormArray<FormGroup<T>>;
186-
187-
array<T extends AbstractControl<any>>(
188-
controls: Array<FormArray<T>>,
189-
validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null,
190-
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormArray<FormArray<T>>;
191-
192-
array<T extends AbstractControl<any>>(
193-
controls: Array<AbstractControl<T>>,
194-
validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null,
195-
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormArray<AbstractControl<T>>;
196-
197-
array<T>(
198-
controls: Array<FormControlState<T>|ControlConfig<T>|T>,
199-
validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null,
200-
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormArray<FormControl<T|null>>;
201-
202193
/**
203194
* Constructs a new `FormArray` from the given array of configurations,
204195
* validators and options.
@@ -212,11 +203,12 @@ export class FormBuilder {
212203
*
213204
* @param asyncValidator A single async validator or array of async validator functions.
214205
*/
215-
array(
216-
controls: any[], validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null,
217-
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormArray {
206+
array<T>(
207+
controls: Array<T>, validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null,
208+
asyncValidator?: AsyncValidatorFn|AsyncValidatorFn[]|null): FormArray<ɵElement<T>> {
218209
const createdControls = controls.map(c => this._createControl(c));
219-
return new FormArray(createdControls, validatorOrOpts, asyncValidator);
210+
// Cast to `any` because the inferred types are not as specific as Element.
211+
return new FormArray(createdControls, validatorOrOpts, asyncValidator) as any;
220212
}
221213

222214
/** @internal */
@@ -261,13 +253,16 @@ export class UntypedFormBuilder extends FormBuilder {
261253
controlsConfig: {[key: string]: any},
262254
options?: AbstractControlOptions|null,
263255
): UntypedFormGroup;
256+
264257
/**
265-
* @deprecated
258+
* @deprecated This API is not typesafe and can result in issues with Closure Compiler renaming.
259+
* Use the `FormBuilder#group` overload with `AbstractControlOptions` instead.
266260
*/
267261
override group(
268262
controlsConfig: {[key: string]: any},
269263
options: {[key: string]: any},
270264
): UntypedFormGroup;
265+
271266
override group(
272267
controlsConfig: {[key: string]: any},
273268
options: AbstractControlOptions|{[key: string]: any}|null = null): UntypedFormGroup {

packages/forms/src/forms.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export {FormArrayName, FormGroupName} from './directives/reactive_directives/for
4141
export {NgSelectOption, SelectControlValueAccessor} from './directives/select_control_value_accessor';
4242
export {SelectMultipleControlValueAccessor, ɵNgSelectMultipleOption} from './directives/select_multiple_control_value_accessor';
4343
export {AsyncValidator, AsyncValidatorFn, CheckboxRequiredValidator, EmailValidator, MaxLengthValidator, MaxValidator, MinLengthValidator, MinValidator, PatternValidator, RequiredValidator, ValidationErrors, Validator, ValidatorFn} from './directives/validators';
44-
export {FormBuilder, UntypedFormBuilder, ɵGroupElement} from './form_builder';
44+
export {FormBuilder, UntypedFormBuilder, ɵElement} from './form_builder';
4545
export {AbstractControl, AbstractControlOptions, FormControlStatus, ɵCoerceStrArrToNumArr, ɵGetProperty, ɵNavigate, ɵRawValue, ɵTokenize, ɵTypedOrUntyped, ɵValue, ɵWriteable} from './model/abstract_model';
4646
export {FormArray, UntypedFormArray, ɵFormArrayRawValue, ɵFormArrayValue} from './model/form_array';
4747
export {FormControl, FormControlOptions, FormControlState, UntypedFormControl, ɵFormControlCtor} from './model/form_control';

packages/forms/test/typed_integration_spec.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,6 +1052,37 @@ describe('Typed Class', () => {
10521052
}
10531053
});
10541054

1055+
it('from objects with ControlConfigs and validators', () => {
1056+
const c = fb.group({foo: ['bar', Validators.required]});
1057+
{
1058+
type ControlsType = {foo: FormControl<string|null>};
1059+
let t: ControlsType = c.controls;
1060+
let t1 = c.controls;
1061+
t1 = null as unknown as ControlsType;
1062+
}
1063+
});
1064+
1065+
it('from objects with ControlConfigs and validator lists', () => {
1066+
const c = fb.group({foo: ['bar', [Validators.required, Validators.email]]});
1067+
{
1068+
type ControlsType = {foo: FormControl<string|null>};
1069+
let t: ControlsType = c.controls;
1070+
let t1 = c.controls;
1071+
t1 = null as unknown as ControlsType;
1072+
}
1073+
});
1074+
1075+
it('from objects with ControlConfigs and explicit types', () => {
1076+
const c: FormGroup<{foo: FormControl<string|null>}> =
1077+
fb.group({foo: ['bar', [Validators.required, Validators.email]]});
1078+
{
1079+
type ControlsType = {foo: FormControl<string|null>};
1080+
let t: ControlsType = c.controls;
1081+
let t1 = c.controls;
1082+
t1 = null as unknown as ControlsType;
1083+
}
1084+
});
1085+
10551086
describe('from objects with FormControls', () => {
10561087
it('nullably', () => {
10571088
const c = fb.group({foo: new FormControl('bar')});
@@ -1233,6 +1264,7 @@ describe('Typed Class', () => {
12331264
}>>,
12341265
};
12351266
let t: ControlType = myParty.controls;
1267+
let d = myParty.controls.dinnerOptions;
12361268
let t1 = myParty.controls;
12371269
t1 = null as unknown as ControlType;
12381270
}

0 commit comments

Comments
 (0)