From dba6a608616a98ec7aee489cbb336d747954d8a3 Mon Sep 17 00:00:00 2001 From: Dylan Hunn Date: Thu, 12 May 2022 15:29:03 -0700 Subject: [PATCH] fix(forms): Value and RawValue should be part of the public API. (#45978) Consider a typed group for storing contact information: ``` declare interface ContactControls { name: FormControl; } contactForm: FormGroup = ...; saveForm(form: FormGroup) { service.newContact(contactForm.value); } ``` What should be the type of `newContact`? The answer, of course, is the value type: ``` declare interface Contact { name: string|null; } class ContactService { newContact(c: Contact) {} } ``` This is quite redundant, and therefore, we should allow the value type to be generated automatically. We already have the helper types to do this -- we just need to document and export them. Then, this becomes possible: ``` class ContactService { newContact(c: RawValue>) {} } ``` PR Close #45978 --- goldens/public-api/forms/index.md | 16 +++-- packages/forms/src/forms.ts | 2 +- packages/forms/src/model/abstract_model.ts | 79 ++++++++++++++++++++-- packages/forms/src/model/form_array.ts | 6 +- packages/forms/src/model/form_group.ts | 16 ++--- 5 files changed, 96 insertions(+), 23 deletions(-) diff --git a/goldens/public-api/forms/index.md b/goldens/public-api/forms/index.md index d65d558967b..ca01ee9876b 100644 --- a/goldens/public-api/forms/index.md +++ b/goldens/public-api/forms/index.md @@ -494,7 +494,7 @@ export class FormGroupName extends AbstractFormGroupDirective implements OnInit, } // @public (undocumented) -export class FormRecord, ɵRawValue> = AbstractControl> extends FormGroup<{ +export class FormRecord, RawValue> = AbstractControl> extends FormGroup<{ [key: string]: TControl; }> { } @@ -506,10 +506,10 @@ export interface FormRecord { }): void; contains(controlName: string): boolean; getRawValue(): { - [key: string]: ɵRawValue; + [key: string]: RawValue; }; patchValue(value: { - [key: string]: ɵValue; + [key: string]: Value; }, options?: { onlySelf?: boolean; emitEvent?: boolean; @@ -519,7 +519,7 @@ export interface FormRecord { emitEvent?: boolean; }): void; reset(value?: { - [key: string]: ɵValue; + [key: string]: Value; }, options?: { onlySelf?: boolean; emitEvent?: boolean; @@ -528,7 +528,7 @@ export interface FormRecord { emitEvent?: boolean; }): void; setValue(value: { - [key: string]: ɵValue; + [key: string]: Value; }, options?: { onlySelf?: boolean; emitEvent?: boolean; @@ -767,6 +767,9 @@ export class RangeValueAccessor extends BuiltInControlValueAccessor implements C static ɵfac: i0.ɵɵFactoryDeclaration; } +// @public +export type RawValue = T extends AbstractControl ? (T['setValue'] extends ((v: infer R) => void) ? R : never) : never; + // @public export class ReactiveFormsModule { static withConfig(opts: { @@ -887,6 +890,9 @@ export class Validators { static requiredTrue(control: AbstractControl): ValidationErrors | null; } +// @public +export type Value = T extends AbstractControl ? T['value'] : never; + // @public (undocumented) export const VERSION: Version; diff --git a/packages/forms/src/forms.ts b/packages/forms/src/forms.ts index 98ddd8d923f..715d0b183e3 100644 --- a/packages/forms/src/forms.ts +++ b/packages/forms/src/forms.ts @@ -42,7 +42,7 @@ export {NgSelectOption, SelectControlValueAccessor} from './directives/select_co export {SelectMultipleControlValueAccessor, ɵNgSelectMultipleOption} from './directives/select_multiple_control_value_accessor'; export {AsyncValidator, AsyncValidatorFn, CheckboxRequiredValidator, EmailValidator, MaxLengthValidator, MaxValidator, MinLengthValidator, MinValidator, PatternValidator, RequiredValidator, ValidationErrors, Validator, ValidatorFn} from './directives/validators'; export {FormBuilder, NonNullableFormBuilder, UntypedFormBuilder, ɵElement} from './form_builder'; -export {AbstractControl, AbstractControlOptions, FormControlStatus, ɵCoerceStrArrToNumArr, ɵGetProperty, ɵNavigate, ɵRawValue, ɵTokenize, ɵTypedOrUntyped, ɵValue, ɵWriteable} from './model/abstract_model'; +export {AbstractControl, AbstractControlOptions, FormControlStatus, RawValue, Value, ɵCoerceStrArrToNumArr, ɵGetProperty, ɵNavigate, ɵTokenize, ɵTypedOrUntyped, ɵWriteable} from './model/abstract_model'; export {FormArray, UntypedFormArray, ɵFormArrayRawValue, ɵFormArrayValue} from './model/form_array'; export {FormControl, FormControlOptions, FormControlState, UntypedFormControl, ɵFormControlCtor} from './model/form_control'; export {FormGroup, FormRecord, UntypedFormGroup, ɵFormGroupRawValue, ɵFormGroupValue, ɵOptionalKeys} from './model/form_group'; diff --git a/packages/forms/src/model/abstract_model.ts b/packages/forms/src/model/abstract_model.ts index 5a3f8ddf827..a401d755d18 100644 --- a/packages/forms/src/model/abstract_model.ts +++ b/packages/forms/src/model/abstract_model.ts @@ -166,19 +166,86 @@ export type ɵIsAny = 0 extends(1&T) ? Y : N; export type ɵTypedOrUntyped = ɵIsAny; /** - * Value gives the type of `.value` in an `AbstractControl`. + * Value gives the value type corresponding to a control type. * - * For internal use only. + * Note that the resulting type will follow the same rules as `.value` on your control, group, or + * array, including `undefined` for each group element which might be disabled. + * + * If you are trying to extract a value type for a data model, you probably want {@see RawValue}, + * which will not have `undefined` in group keys. + * + * @usageNotes + * + * ### `FormControl` value type + * + * You can extract the value type of a single control: + * + * ```ts + * type NameControl = FormControl; + * type NameValue = Value; + * ``` + * + * The resulting type is `string`. + * + * ### `FormGroup` value type + * + * Imagine you have an interface defining the controls in your group. You can extract the shape of + * the values as follows: + * + * ```ts + * interface PartyFormControls { + * address: FormControl; + * } + * + * // Value operates on controls; the object must be wrapped in a FormGroup. + * type PartyFormValues = Value>; + * ``` + * + * The resulting type is `{address: string|undefined}`. + * + * ### `FormArray` value type + * + * You can extract values from FormArrays as well: + * + * ```ts + * type GuestNamesControls = FormArray>; + * + * type NamesValues = Value; + * ``` + * + * The resulting type is `string[]`. */ -export type ɵValue = +export type Value = T extends AbstractControl? T['value'] : never; /** - * RawValue gives the type of `.getRawValue()` in an `AbstractControl`. + * RawValue gives the raw value type corresponding to a control type. * - * For internal use only. + * Note that the resulting type will follow the same rules as `.getRawValue()` on your control, + * group, or array. This means that all controls inside a group will be required, not optional, + * regardless of their disabled state. + * + * You may also wish to use {@see Value}, which will have `undefined` in group keys (which can be disabled). + * + * @usageNotes + * + * ### `FormGroup` raw value type + * + * Imagine you have an interface defining the controls in your group. You can extract the shape of + * the raw values as follows: + * + * ```ts + * interface PartyFormControls { + * address: FormControl; + * } + * + * // RawValue operates on controls; the object must be wrapped in a FormGroup. + * type PartyFormValues = RawValue>; + * ``` + * + * The resulting type is `{address: string}`. (Note the absence of `undefined`.) */ -export type ɵRawValue = T extends AbstractControl? +export type RawValue = T extends AbstractControl? (T['setValue'] extends((v: infer R) => void) ? R : never) : never; diff --git a/packages/forms/src/model/form_array.ts b/packages/forms/src/model/form_array.ts index 20fefdf27b9..5c5ae9ecbcb 100644 --- a/packages/forms/src/model/form_array.ts +++ b/packages/forms/src/model/form_array.ts @@ -8,7 +8,7 @@ import {AsyncValidatorFn, ValidatorFn} from '../directives/validators'; -import {AbstractControl, AbstractControlOptions, assertAllValuesPresent, assertControlPresent, pickAsyncValidators, pickValidators, ɵRawValue, ɵTypedOrUntyped, ɵValue} from './abstract_model'; +import {AbstractControl, AbstractControlOptions, assertAllValuesPresent, assertControlPresent, pickAsyncValidators, pickValidators, RawValue, Value, ɵTypedOrUntyped} from './abstract_model'; /** * FormArrayValue extracts the type of `.value` from a FormArray's element type, and wraps it in an @@ -18,7 +18,7 @@ import {AbstractControl, AbstractControlOptions, assertAllValuesPresent, assertC * case falls back to any[]. */ export type ɵFormArrayValue> = - ɵTypedOrUntyped>, any[]>; + ɵTypedOrUntyped>, any[]>; /** * FormArrayRawValue extracts the type of `.getRawValue()` from a FormArray's element type, and @@ -27,7 +27,7 @@ export type ɵFormArrayValue> = * Angular uses this type internally to support Typed Forms; do not use it directly. */ export type ɵFormArrayRawValue> = - ɵTypedOrUntyped>, any[]>; + ɵTypedOrUntyped>, any[]>; /** * Tracks the value and validity state of an array of `FormControl`, diff --git a/packages/forms/src/model/form_group.ts b/packages/forms/src/model/form_group.ts index 5c8335465ea..8aaf64480d1 100644 --- a/packages/forms/src/model/form_group.ts +++ b/packages/forms/src/model/form_group.ts @@ -8,7 +8,7 @@ import {AsyncValidatorFn, ValidatorFn} from '../directives/validators'; -import {AbstractControl, AbstractControlOptions, assertAllValuesPresent, assertControlPresent, pickAsyncValidators, pickValidators, ɵRawValue, ɵTypedOrUntyped, ɵValue} from './abstract_model'; +import {AbstractControl, AbstractControlOptions, assertAllValuesPresent, assertControlPresent, pickAsyncValidators, pickValidators, RawValue, Value, ɵTypedOrUntyped} from './abstract_model'; /** * FormGroupValue extracts the type of `.value` from a FormGroup's inner object type. The untyped @@ -19,7 +19,7 @@ import {AbstractControl, AbstractControlOptions, assertAllValuesPresent, assertC * For internal use only. */ export type ɵFormGroupValue}> = - ɵTypedOrUntyped}>, {[key: string]: any}>; + ɵTypedOrUntyped}>, {[key: string]: any}>; /** * FormGroupRawValue extracts the type of `.getRawValue()` from a FormGroup's inner object type. The @@ -30,7 +30,7 @@ export type ɵFormGroupValue}> * For internal use only. */ export type ɵFormGroupRawValue}> = - ɵTypedOrUntyped}, {[key: string]: any}>; + ɵTypedOrUntyped}, {[key: string]: any}>; /** * OptionalKeys returns the union of all optional keys in the object. @@ -606,7 +606,7 @@ export const UntypedFormGroup: UntypedFormGroupCtor = FormGroup; export const isFormGroup = (control: unknown): control is FormGroup => control instanceof FormGroup; -export class FormRecord, ɵRawValue> = +export class FormRecord, RawValue> = AbstractControl> extends FormGroup<{[key: string]: TControl}> {} @@ -671,7 +671,7 @@ export interface FormRecord { * * {@see FormGroup#setValue} */ - setValue(value: {[key: string]: ɵValue}, options?: { + setValue(value: {[key: string]: Value}, options?: { onlySelf?: boolean, emitEvent?: boolean }): void; @@ -683,7 +683,7 @@ export interface FormRecord { * * {@see FormGroup#patchValue} */ - patchValue(value: {[key: string]: ɵValue}, options?: { + patchValue(value: {[key: string]: Value}, options?: { onlySelf?: boolean, emitEvent?: boolean }): void; @@ -694,7 +694,7 @@ export interface FormRecord { * * {@see FormGroup#reset} */ - reset(value?: {[key: string]: ɵValue}, options?: { + reset(value?: {[key: string]: Value}, options?: { onlySelf?: boolean, emitEvent?: boolean }): void; @@ -704,7 +704,7 @@ export interface FormRecord { * * {@see FormGroup#getRawValue} */ - getRawValue(): {[key: string]: ɵRawValue}; + getRawValue(): {[key: string]: RawValue}; } export const isFormRecord = (control: unknown): control is FormRecord =>