Tipado avanzado
Operador de opcionalidad '?'
El operador '?', además de para las expresiones ternarias, también se puede utilizar en más ámbitos. Por ejemplo, para indicar que un parámetro en una función es opcional (puede tomar el valor undefined). Otra forma de hacer un parámetro opcional sería darle un valor por defecto.
También se puede utilizar para indicar en una clase o interfaz que un atributo puede no tener valor (undefined). Es decir, que es opcional, sin que nos marque ningún error. Debemos tener en cuenta que el tipo del atributo en ese caso, será el que pongamos unido con undefined (ejemplo: number | undefined).
También se puede utilizar en objetos que pueden ser undefined o null, a la hora de acceder a sus propiedades o métodos, que no de error. En ese caso devuelve el valor undefined o null del objeto directamente sin acceder a la propiedad o método. Este uso también existe en JavaScript.
Operador Non-null '!'
El operador '!' en TypeScript sirve para indicar que estamos seguros de que un valor que para TypeScript puede ser undefined o null, tiene valor. Hay que utilizarlo con mucho cuidado, ya que realmente debemos tener esa seguridad o se producirá un error cuando accedamos a alguna de sus propiedades.
En el siguiente ejemplo vemos como cuando una función puede o no devolver un valor (undefined o null), TypeScript tiene en consideración a la hora de tratarlo que la variable puede no tener valor, por lo que nos avisa con un error. Con el operador '!' anulamos dicho error indicando que estamos seguros de que tiene valor.
Este operador se debe utilizar con responsabilidad. Es decir, cuando estamos 100% seguros de que existe un valor asignado (por ejemplo, si lo hemos comprobado antes). En otro caso es mejor siempre contar con la posibilidad de que pueda ser undefined y comprobarlo (o usar el operador ? para que si es undefined no haga nada).
Aquí vemos otro ejemplo donde podemos utilizarlo en una clase para indicar a TypeScript que aunque no vea que le hemos asignado un valor explícito por defecto o en el constructor (es decir, piense que se queda como undefined), estamos seguros de que va a tener valor.
Si por ejemplo, creamos el objeto a partir de un método estático, TypeScript solo comprueba el valor por defecto en los atributos o su asignación en el constructor, por lo que se piensa que van a ser undefined.
El código de arriba debería funcionar, por lo que para que el compilador de TypeScript (si así está configurado) no nos marque estos atributo con un error por no haber sido asignados, indicamos con '!' que ambos atributos van a tener un valor seguro. Es importante recordar que este operador solo se debe usar cuando estemos 100% seguros.
Tipos genéricos
Los tipos genéricos o generics permite crear funciones y clases que trabajen con diferentes tipos de datos sin tener que especificar el tipo a la hora de definir dichas funciones y clases, sino a la hora de llamar a la función o crear el objeto.
Para definir un tipo genérico en TypeScript, se utiliza la sintaxis <T>, donde T es una variable de tipo que representa el tipo de dato que se pasará como argumento en tiempo de ejecución.
Tipos genéricos en funciones/métodos
Cuando no sabemos el tipo de algún dato con el que va a trabajar una función o método a la hora de declararlo, ya sea el tipo de un parámetro o el valor devuelto, podemos hacer 2 cosas:
- Tratar este tipo como any (no recomendado) ya que perdemos la ventajas del tipado.
- Utilizar un tipo genérico y definirlo a la hora de llamar a la función.
El siguiente ejemplo es el de una clase llamada Http, que permite hacer llamadas a servicios web utilizando la API fetch. Para ser capaz de tipar la respuesta del servidor, teniendo en cuenta que según la llamada que hagamos van a ser diferentes datos, podemos usar tipos genéricos en los métodos.
De esta manera, a la hora de hacer una llamada podemos tipar la respuesta del servidor directamente sin tener que hacer un casting de any (tipo por defecto) al tipo que sea posteriormente.
Podemos ir un paso más allá y tipar los datos que le enviamos al servidor en las llamadas POST y PUT. Para eso añadimos un segundo tipo genérico 'U' que se le aplicará al parámetro del método correspondiente.
El objetivo de hacer esto es que nos va a obligar a pasarle un objeto del tipo especificado al llamar al método, o al menos que tenga los mismos atributos. Así nos aseguramos que enviamos el dato correcto, y si no, TypeScript nos indicaría un error.
Tipos genéricos en clases
Los tipos genéricos se pueden definir a nivel de clase en lugar de método. En este caso, el tipo se especificaría al crear el objeto.
Restricciones en los tipos genéricos
Por último, se puede restringir el tipo de dato que le pasemos con la instrucción extends. De esta manera sólo podemos pasar parámetros de ese tipo o derivados.
Herramientas para el tipado de objetos
Si queremos crear un derivado a partir de un tipo existente, por ejemplo, que todos los campos sean opcionales, u obligatorios, TypeScript tiene una serie de herramientas que nos pueden ayudar a crear dichos tipos derivados. Vamos a ver algunos de ellos.
Vamos a basarnos en esta interfaz para los ejemplos:
Partial
Permite crear un tipo derivado donde todos los atributos son opcionales
Required
Todo lo contrario a Partial. Todos los campos de la interfaz son obligatorios.
Readonly
En este caso los campos son de solo lectura. Solo se les puede dar valor inicial al crear el objeto.
Puedes consultar más herramientas para crear tipos derivados en la documentación oficial.