Skip to content

Commit 89d2991

Browse files
dylhunnthePunderWoman
authored andcommitted
feat(forms): Implement strict types for the Angular Forms package. (angular#43834)
This PR strongly types the forms package by adding generics to AbstractControl classes as well as FormBuilder. This makes forms type-safe and null-safe, for both controls and values. The design uses a "control-types" approach. In other words, the type parameter on FormGroup is an object containing controls, and the type parameter on FormArray is an array of controls. Special thanks to Alex Rickabaugh and Andrew Kushnir for co-design & implementation, to Sonu Kapoor and Netanel Basal for illustrative prior art, and to Cédric Exbrayat for extensive testing and validation. BREAKING CHANGE: Forms classes accept a generic. Forms model classes now accept a generic type parameter. Untyped versions of these classes are available to opt-out of the new, stricter behavior. PR Close angular#43834
1 parent d11d1c0 commit 89d2991

18 files changed

Lines changed: 2059 additions & 314 deletions

File tree

aio/content/examples/ngmodules/src/app/contact/contact.component.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
// Exact copy except import UserService from greeting
2-
import { Component, OnInit } from '@angular/core';
3-
import { FormBuilder, Validators } from '@angular/forms';
2+
import {Component, OnInit} from '@angular/core';
3+
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
44

5-
import { Contact, ContactService } from './contact.service';
6-
import { UserService } from '../greeting/user.service';
5+
import {UserService} from '../greeting/user.service';
6+
7+
import {Contact, ContactService} from './contact.service';
78

89
@Component({
910
selector: 'app-contact',
1011
templateUrl: './contact.component.html',
11-
styleUrls: [ './contact.component.css' ]
12+
styleUrls: ['./contact.component.css']
1213
})
1314
export class ContactComponent implements OnInit {
1415
contact!: Contact;
@@ -17,11 +18,11 @@ export class ContactComponent implements OnInit {
1718
msg = 'Loading contacts ...';
1819
userName = '';
1920

20-
contactForm = this.fb.group({
21-
name: ['', Validators.required]
22-
});
21+
contactForm: FormGroup;
2322

24-
constructor(private contactService: ContactService, userService: UserService, private fb: FormBuilder) {
23+
constructor(
24+
private contactService: ContactService, userService: UserService, private fb: FormBuilder) {
25+
this.contactForm = this.fb.group({name: ['', Validators.required]});
2526
this.userName = userService.userName;
2627
}
2728

@@ -40,7 +41,9 @@ export class ContactComponent implements OnInit {
4041

4142
next() {
4243
let ix = 1 + this.contacts.indexOf(this.contact);
43-
if (ix >= this.contacts.length) { ix = 0; }
44+
if (ix >= this.contacts.length) {
45+
ix = 0;
46+
}
4447
this.contact = this.contacts[ix];
4548
console.log(this.contacts[ix]);
4649
}

goldens/public-api/forms/index.md

Lines changed: 105 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { SimpleChanges } from '@angular/core';
2121
import { Version } from '@angular/core';
2222

2323
// @public
24-
export abstract class AbstractControl {
24+
export abstract class AbstractControl<TValue = any, TRawValue extends TValue = TValue> {
2525
constructor(validators: ValidatorFn | ValidatorFn[] | null, asyncValidators: AsyncValidatorFn | AsyncValidatorFn[] | null);
2626
addAsyncValidators(validators: AsyncValidatorFn | AsyncValidatorFn[]): void;
2727
addValidators(validators: ValidatorFn | ValidatorFn[]): void;
@@ -41,7 +41,8 @@ export abstract class AbstractControl {
4141
}): void;
4242
get enabled(): boolean;
4343
readonly errors: ValidationErrors | null;
44-
get(path: Array<string | number> | string): AbstractControl | null;
44+
get<P extends string | (readonly (string | number)[])>(path: P): AbstractControlGetProperty<TRawValue, P>> | null;
45+
get<P extends string | Array<string | number>>(path: P): AbstractControlGetProperty<TRawValue, P>> | null;
4546
getError(errorCode: string, path?: Array<string | number> | string): any;
4647
getRawValue(): any;
4748
hasAsyncValidator(validator: AsyncValidatorFn): boolean;
@@ -66,21 +67,20 @@ export abstract class AbstractControl {
6667
onlySelf?: boolean;
6768
}): void;
6869
get parent(): FormGroup | FormArray | null;
69-
abstract patchValue(value: any, options?: Object): void;
70+
abstract patchValue(value: TValue, options?: Object): void;
7071
get pending(): boolean;
7172
readonly pristine: boolean;
7273
removeAsyncValidators(validators: AsyncValidatorFn | AsyncValidatorFn[]): void;
7374
removeValidators(validators: ValidatorFn | ValidatorFn[]): void;
74-
abstract reset(value?: any, options?: Object): void;
75+
abstract reset(value?: TValue, options?: Object): void;
7576
get root(): AbstractControl;
7677
setAsyncValidators(validators: AsyncValidatorFn | AsyncValidatorFn[] | null): void;
7778
setErrors(errors: ValidationErrors | null, opts?: {
7879
emitEvent?: boolean;
7980
}): void;
80-
// (undocumented)
81-
setParent(parent: FormGroup | FormArray): void;
81+
setParent(parent: FormGroup | FormArray | null): void;
8282
setValidators(validators: ValidatorFn | ValidatorFn[] | null): void;
83-
abstract setValue(value: any, options?: Object): void;
83+
abstract setValue(value: TRawValue, options?: Object): void;
8484
readonly status: FormControlStatus;
8585
readonly statusChanges: Observable<FormControlStatus>;
8686
readonly touched: boolean;
@@ -93,8 +93,8 @@ export abstract class AbstractControl {
9393
get valid(): boolean;
9494
get validator(): ValidatorFn | null;
9595
set validator(validatorFn: ValidatorFn | null);
96-
readonly value: any;
97-
readonly valueChanges: Observable<any>;
96+
readonly value: TValue;
97+
readonly valueChanges: Observable<TValue>;
9898
}
9999

100100
// @public
@@ -223,37 +223,37 @@ export interface Form {
223223
}
224224

225225
// @public
226-
export class FormArray extends AbstractControl {
227-
constructor(controls: AbstractControl[], validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null);
228-
at(index: number): AbstractControl;
226+
export class FormArray<TControl extends AbstractControl<any> = any> extends AbstractControlTypedOrUntyped<TControl, ɵFormArrayValue<TControl>, any>, ɵTypedOrUntyped<TControl, ɵFormArrayRawValue<TControl>, any>> {
227+
constructor(controls: Array<TControl>, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null);
228+
at(index: number): ɵTypedOrUntyped<TControl, TControl, AbstractControl<any>>;
229229
clear(options?: {
230230
emitEvent?: boolean;
231231
}): void;
232232
// (undocumented)
233-
controls: AbstractControl[];
234-
getRawValue(): any[];
235-
insert(index: number, control: AbstractControl, options?: {
233+
controls: ɵTypedOrUntyped<TControl, Array<TControl>, Array<AbstractControl<any>>>;
234+
getRawValue(): ɵFormArrayRawValue<TControl>;
235+
insert(index: number, control: TControl, options?: {
236236
emitEvent?: boolean;
237237
}): void;
238238
get length(): number;
239-
patchValue(value: any[], options?: {
239+
patchValue(value: ɵFormArrayValue<TControl>, options?: {
240240
onlySelf?: boolean;
241241
emitEvent?: boolean;
242242
}): void;
243-
push(control: AbstractControl, options?: {
243+
push(control: TControl, options?: {
244244
emitEvent?: boolean;
245245
}): void;
246246
removeAt(index: number, options?: {
247247
emitEvent?: boolean;
248248
}): void;
249-
reset(value?: any, options?: {
249+
reset(value?: ɵTypedOrUntyped<TControl, ɵFormArrayValue<TControl>, any>, options?: {
250250
onlySelf?: boolean;
251251
emitEvent?: boolean;
252252
}): void;
253-
setControl(index: number, control: AbstractControl, options?: {
253+
setControl(index: number, control: TControl, options?: {
254254
emitEvent?: boolean;
255255
}): void;
256-
setValue(value: any[], options?: {
256+
setValue(value: ɵFormArrayRawValue<TControl>, options?: {
257257
onlySelf?: boolean;
258258
emitEvent?: boolean;
259259
}): void;
@@ -276,13 +276,32 @@ export class FormArrayName extends ControlContainer implements OnInit, OnDestroy
276276

277277
// @public
278278
export class FormBuilder {
279-
array(controlsConfig: any[], validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormArray;
280-
control(formState: any, validatorOrOpts?: ValidatorFn | ValidatorFn[] | FormControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormControl;
281-
group(controlsConfig: {
282-
[key: string]: any;
283-
}, options?: AbstractControlOptions | null): FormGroup;
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>>;
291+
// (undocumented)
292+
control<T>(formState: T | FormControlState<T>, opts: FormControlOptions & {
293+
initialValueIsDefault: true;
294+
}): FormControl<T>;
295+
// (undocumented)
296+
control<T>(formState: T | FormControlState<T>, validatorOrOpts?: ValidatorFn | ValidatorFn[] | FormControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null): FormControl<T | null>;
297+
// (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]>;
302+
}>;
284303
// @deprecated
285-
group(controlsConfig: {
304+
group(controls: {
286305
[key: string]: any;
287306
}, options: {
288307
[key: string]: any;
@@ -294,21 +313,22 @@ export class FormBuilder {
294313
}
295314

296315
// @public
297-
export interface FormControl extends AbstractControl {
298-
readonly defaultValue: any;
299-
patchValue(value: any, options?: {
316+
export interface FormControl<TValue = any> extends AbstractControl<TValue> {
317+
readonly defaultValue: TValue;
318+
getRawValue(): TValue;
319+
patchValue(value: TValue, options?: {
300320
onlySelf?: boolean;
301321
emitEvent?: boolean;
302322
emitModelToViewChange?: boolean;
303323
emitViewToModelChange?: boolean;
304324
}): void;
305325
registerOnChange(fn: Function): void;
306326
registerOnDisabledChange(fn: (isDisabled: boolean) => void): void;
307-
reset(formState?: any, options?: {
327+
reset(formState?: TValue | FormControlState<TValue>, options?: {
308328
onlySelf?: boolean;
309329
emitEvent?: boolean;
310330
}): void;
311-
setValue(value: any, options?: {
331+
setValue(value: TValue, options?: {
312332
onlySelf?: boolean;
313333
emitEvent?: boolean;
314334
emitModelToViewChange?: boolean;
@@ -370,43 +390,74 @@ export interface FormControlOptions extends AbstractControlOptions {
370390
initialValueIsDefault?: boolean;
371391
}
372392

393+
// @public
394+
export interface FormControlState<T> {
395+
// (undocumented)
396+
disabled: boolean;
397+
// (undocumented)
398+
value: T;
399+
}
400+
373401
// @public
374402
export type FormControlStatus = 'VALID' | 'INVALID' | 'PENDING' | 'DISABLED';
375403

376404
// @public
377-
export class FormGroup extends AbstractControl {
378-
constructor(controls: {
379-
[key: string]: AbstractControl;
380-
}, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null);
381-
addControl(name: string, control: AbstractControl, options?: {
405+
export class FormGroup<TControl extends {
406+
[K in keyof TControl]: AbstractControl<any>;
407+
} = any> extends AbstractControlTypedOrUntyped<TControl, ɵFormGroupValue<TControl>, any>, ɵTypedOrUntyped<TControl, ɵFormGroupRawValue<TControl>, any>> {
408+
constructor(controls: TControl, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null);
409+
addControl(this: FormGroup<{
410+
[key: string]: AbstractControl<any>;
411+
}>, name: string, control: AbstractControl, options?: {
382412
emitEvent?: boolean;
383413
}): void;
384-
contains(controlName: string): boolean;
385414
// (undocumented)
386-
controls: {
387-
[key: string]: AbstractControl;
388-
};
389-
getRawValue(): any;
390-
patchValue(value: {
391-
[key: string]: any;
392-
}, options?: {
415+
addControl<K extends string & keyof TControl>(name: K, control: Required<TControl>[K], options?: {
416+
emitEvent?: boolean;
417+
}): void;
418+
contains<K extends string>(controlName: K): boolean;
419+
// (undocumented)
420+
contains(this: FormGroup<{
421+
[key: string]: AbstractControl<any>;
422+
}>, controlName: string): boolean;
423+
// (undocumented)
424+
controls: ɵTypedOrUntyped<TControl, TControl, {
425+
[key: string]: AbstractControl<any>;
426+
}>;
427+
getRawValue(): ɵTypedOrUntyped<TControl, ɵFormGroupRawValue<TControl>, any>;
428+
patchValue(value: ɵFormGroupValue<TControl>, options?: {
393429
onlySelf?: boolean;
394430
emitEvent?: boolean;
395431
}): void;
396-
registerControl(name: string, control: AbstractControl): AbstractControl;
397-
removeControl(name: string, options?: {
432+
registerControl<K extends string & keyof TControl>(name: K, control: TControl[K]): TControl[K];
433+
// (undocumented)
434+
registerControl(this: FormGroup<{
435+
[key: string]: AbstractControl<any>;
436+
}>, name: string, control: AbstractControl<any>): AbstractControl<any>;
437+
// (undocumented)
438+
removeControl(this: FormGroup<{
439+
[key: string]: AbstractControl<any>;
440+
}>, name: string, options?: {
441+
emitEvent?: boolean;
442+
}): void;
443+
// (undocumented)
444+
removeControl<S extends string>(name: ɵOptionalKeys<TControl> & S, options?: {
398445
emitEvent?: boolean;
399446
}): void;
400-
reset(value?: any, options?: {
447+
reset(value?: ɵTypedOrUntyped<TControl, ɵFormGroupValue<TControl>, any>, options?: {
401448
onlySelf?: boolean;
402449
emitEvent?: boolean;
403450
}): void;
404-
setControl(name: string, control: AbstractControl, options?: {
451+
setControl<K extends string & keyof TControl>(name: K, control: TControl[K], options?: {
405452
emitEvent?: boolean;
406453
}): void;
407-
setValue(value: {
408-
[key: string]: any;
409-
}, options?: {
454+
// (undocumented)
455+
setControl(this: FormGroup<{
456+
[key: string]: AbstractControl<any>;
457+
}>, name: string, control: AbstractControl, options?: {
458+
emitEvent?: boolean;
459+
}): void;
460+
setValue(value: ɵFormGroupRawValue<TControl>, options?: {
410461
onlySelf?: boolean;
411462
emitEvent?: boolean;
412463
}): void;
@@ -724,7 +775,7 @@ export class SelectMultipleControlValueAccessor extends BuiltInControlValueAcces
724775
}
725776

726777
// @public
727-
export type UntypedFormArray = FormArray;
778+
export type UntypedFormArray = FormArray<any>;
728779

729780
// @public (undocumented)
730781
export const UntypedFormArray: UntypedFormArrayCtor;
@@ -752,13 +803,13 @@ export class UntypedFormBuilder extends FormBuilder {
752803
}
753804

754805
// @public
755-
export type UntypedFormControl = FormControl;
806+
export type UntypedFormControl = FormControl<any>;
756807

757808
// @public (undocumented)
758809
export const UntypedFormControl: UntypedFormControlCtor;
759810

760811
// @public
761-
export type UntypedFormGroup = FormGroup;
812+
export type UntypedFormGroup = FormGroup<any>;
762813

763814
// @public (undocumented)
764815
export const UntypedFormGroup: UntypedFormGroupCtor;

modules/playground/src/model_driven_forms/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
/* tslint:disable:no-console */
1010
import {Component, Host, NgModule} from '@angular/core';
11-
import {AbstractControl, FormBuilder, FormGroup, FormGroupDirective, ReactiveFormsModule, Validators} from '@angular/forms';
11+
import {AbstractControl, FormBuilder, FormGroup, FormGroupDirective, ReactiveFormsModule, UntypedFormBuilder, UntypedFormGroup, Validators} from '@angular/forms';
1212
import {BrowserModule} from '@angular/platform-browser';
1313
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
1414

@@ -139,10 +139,10 @@ export class ShowError {
139139
`
140140
})
141141
export class ReactiveForms {
142-
form: FormGroup;
142+
form: UntypedFormGroup;
143143
countries = ['US', 'Canada'];
144144

145-
constructor(fb: FormBuilder) {
145+
constructor(fb: UntypedFormBuilder) {
146146
this.form = fb.group({
147147
'firstName': ['', Validators.required],
148148
'middleName': [''],

packages/core/schematics/migrations.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
"factory": "./migrations/entry-components/index"
77
},
88
"migration-v14-typed-forms": {
9-
"version": "9999.0.0",
10-
"description": "Experimental migration that adds <any>s for Typed Forms.",
9+
"version": "14.0.0-beta",
10+
"description": "As of Angular version 14, Forms model classes accept a type parameter, and existing usages must be opted out to preserve backwards-compatibility.",
1111
"factory": "./migrations/typed-forms/index"
1212
},
1313
"migration-v14-path-match-type": {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1095,7 +1095,7 @@
10951095
"name": "isEmptyInputValue"
10961096
},
10971097
{
1098-
"name": "isFormControl"
1098+
"name": "isFormControlState"
10991099
},
11001100
{
11011101
"name": "isForwardRef"
@@ -1353,7 +1353,7 @@
13531353
"name": "removeFromArray"
13541354
},
13551355
{
1356-
"name": "removeListItem"
1356+
"name": "removeListItem2"
13571357
},
13581358
{
13591359
"name": "removeStyle"

0 commit comments

Comments
 (0)