A partir de Angular 14, los formularios son estrictamente tipados por defecto.
Como antecedente para esta guía, te recomendamos familiarizarte con los Formularios Reactivos de Angular.
Visión general de formularios tipados
Con los formularios reactivos de Angular, especificas explícitamente un modelo de formulario. Como ejemplo simple, considera este formulario básico de inicio de sesión:
const login = new FormGroup({ email: new FormControl(''), password: new FormControl(''),});
Angular proporciona muchas APIs para interactuar con este FormGroup
. Por ejemplo, puedes llamar login.value
, login.controls
, login.patchValue
, etc. (Para una referencia completa de la API, consulta la documentación de la API.)
En versiones anteriores de Angular, la mayoría de estas APIs incluían any
en su tipado, e interactuar con la estructura de los controles, o los valores mismos, no era seguro en cuanto a tipos. Por ejemplo: podías escribir el siguiente código inválido:
const emailDomain = login.value.email.domain;
Con formularios reactivos estrictamente tipados, el código anterior no compila, porque email
no tiene la propiedad domain
.
Además de la seguridad, el tipado ofrece otras mejoras, como un mejor autocompletado en los IDE y una forma explícita de especificar la estructura del formulario
Estas mejoras actualmente se aplican solo a formularios reactivos (no a formularios basados en plantillas).
Formularios no tipados
Los formularios no tipados siguen siendo compatibles y continuarán funcionando como antes. Para usarlos, debes importar los símbolos Untyped
de @angular/forms
:
const login = new UntypedFormGroup({ email: new UntypedFormControl(''), password: new UntypedFormControl(''),});
Cada símbolo Untyped
tiene exactamente la misma semántica que en versiones anteriores de Angular. Puedes habilitar el tipado de forma incremental al eliminar los prefijos Untyped
.
FormControl
: Primeros pasos
El formulario más simple consiste en un solo control:
const email = new FormControl('angularrox@gmail.com');
Este control será automáticamente inferido para tener el tipo FormControl<string|null>
. TypeScript aplicará automáticamente este tipo en toda la API de FormControl
, como email.value
, email.valueChanges
, email.setValue(...)
, etc.
Nulabilidad
Podrías preguntarte: ¿por qué el tipo de este control incluye null
? Esto es porque el control puede volverse null
en cualquier momento, llamando a reset:
const email = new FormControl('angularrox@gmail.com');email.reset();console.log(email.value); // null
TypeScript te obligará a manejar la posibilidad de que el control se vuelva null
. Si quieres hacer este control no nullable, puedes usar la opción nonNullable
. Esto hará que el control se resetee a su valor inicial, en lugar de null
:
const email = new FormControl('angularrox@gmail.com', {nonNullable: true});email.reset();console.log(email.value); // angularrox@gmail.com
Cabe reiterar, esta opción afecta el comportamiento del formulario en tiempo de ejecución cuando se llama a .reset()
, y debe cambiarse con cuidado.
Especificar un tipo explícito
Es posible especificar el tipo, en lugar de depender de la inferencia. Considera un control inicializado a null
. Debido a que el valor inicial es null
, TypeScript inferirá FormControl<null>
, lo cual es más restrictivo de lo que queremos.
const email = new FormControl(null);email.setValue('angularrox@gmail.com'); // Error!
Para prevenir esto, especificamos explícitamente el tipo como string|null
:
const email = new FormControl<string|null>(null);email.setValue('angularrox@gmail.com');
FormArray
: Colecciones dinámicas y homogéneas
Un FormArray
contiene una lista abierta de controles. El parámetro de tipo corresponde al tipo de cada control interno:
const names = new FormArray([new FormControl('Alex')]);names.push(new FormControl('Jess'));
Este FormArray
tendrá el tipo de controles internos FormControl<string|null>
.
Si necesitas tener múltiples tipos de elementos dentro del array, debes usar UntypedFormArray
, ya que TypeScript no puede inferir el tipo de elemento en cada posición
FormGroup
y FormRecord
Angular proporciona el tipo FormGroup
para formularios con un conjunto enumerado de claves, y un tipo llamado FormRecord
, para grupos abiertos o dinámicos.
Valores parciales
Considera de nuevo un formulario de inicio de sesión:
const login = new FormGroup({ email: new FormControl('', {nonNullable: true}), password: new FormControl('', {nonNullable: true}),});
En cualquier FormGroup
, es posible deshabilitar controles. Cualquier control deshabilitado no aparecerá en el valor del grupo.
Como consecuencia, el tipo de login.value
es Partial<{email: string, password: string}>
. El Partial
en este tipo significa que cada miembro podría ser undefined
.
Más específicamente, el tipo de login.value.email
es string|undefined
, y TypeScript aplicará que manejes el valor posiblemente undefined
(si tienes strictNullChecks
habilitado).
Si quieres acceder al valor incluyendo controles deshabilitados, y así evitar campos posiblemente undefined
, puedes usar login.getRawValue()
.
Controles opcionales y grupos dinámicos
Algunos formularios tienen controles que pueden o no estar presentes, y que pueden ser agregados o eliminados en tiempo de ejecución. Puedes representar estos controles usando campos opcionales:
interface LoginForm { email: FormControl<string>; password?: FormControl<string>;}const login = new FormGroup<LoginForm>({ email: new FormControl('', {nonNullable: true}), password: new FormControl('', {nonNullable: true}),});login.removeControl('password');
En este formulario especificamos explícitamente el tipo, lo que nos permite hacer que el control password
sea opcional. TypeScript se encargará de aplicar que solo los controles opcionales puedan añadirse o eliminarse.
FormRecord
Algunos usos de FormGroup
no se ajustan al patrón anterior porque las claves no se conocen de antemano. La clase FormRecord
está diseñada para ese caso:
const addresses = new FormRecord<FormControl<string|null>>({});addresses.addControl('Andrew', new FormControl('2340 Folsom St'));
A este FormRecord
se le puede agregar cualquier control de tipo string|null
.
Si necesitas un FormGroup
que sea dinámico (abierto) y heterogéneo (los controles son de diferentes tipos), no es posible mejorar la seguridad de tipos, y deberías usar UntypedFormGroup
.
Un FormRecord
también puede construirse con el FormBuilder
:
const addresses = fb.record({'Andrew': '2340 Folsom St'});
FormBuilder
and NonNullableFormBuilder
La clase FormBuilder
ha sido actualizada para soportar los nuevos tipos también, de la misma manera que los ejemplos anteriores.
Además, hay un constructor adicional disponible: NonNullableFormBuilder
. Este tipo es una abreviatura para especificar {nonNullable: true}
en cada control, y puede eliminar código repetitivo significativo de formularios grandes sin valores nulos. Puedes acceder a él usando la propiedad nonNullable
en un FormBuilder
:
const fb = new FormBuilder();const login = fb.nonNullable.group({ email: '', password: '',});
En el ejemplo anterior, ambos controles internos no permitirán valores nulos (es decir, el valor nonNullable
estará establecido como true
)
También puedes inyectarlo usando el nombre NonNullableFormBuilder
.