En este artículo comentaremos 10 malos hábitos al programar en TypeScript que debemos evitar. Informaremos cuál es el mal hábito, por qué es común tenerlo y cómo podemos evitarlo.
Este artículo es una traducción del artículo original en inglés, hecho por Daniel Bartholomae. Puedes leer el artículo original haciendo clic aquí. ¡Esperamos que les guste!
1 - No usar el modo strict
Cómo puede estar el código
Podríamos crear el archivo tsconfig.json sin definir el modo estricto:
{
"compilerOptions": {
"target": "ES2015",
"module": "commonjs"
}
}
Cómo debería estar el código
Simplemente debemos habilitar el modo estricto:
{
"compilerOptions": {
"target": "ES2015",
"module": "commonjs",
"strict": true
}
}
¿Por qué tenemos este hábito?
Introducir reglas estrictas a una base de código ya existente toma tiempo. Por eso, puede ser que no deseemos habilitar este modo.
¿Por qué deberíamos evitar este hábito?
Las reglas más estrictas harán que sea más fácil cambiar el código en el futuro. Así, valdrá la pena el tiempo que usaremos para corregir los errores, ya que seremos muy recompensados cuando trabajemos con este código en un repositorio en el futuro.
2 - Definir valores predeterminados con el operador ||
Cómo puede estar el código
Podríamos definir valores opcionales usando el operador ||:
function createBlogPost (text: string, author: string, date?: Date) {
return {
text: text,
author: author,
date: date || new Date()
}
}
Cómo debería estar el código
Podemos usar el nuevo operador ??, o, mejor aún, definir los valores predeterminados en el momento de la declaración de los parámetros:
function createBlogPost (text: string, author: string, date: Date = new Date())
return {
text: text,
author: author,
date: date
}
}
¿Por qué tenemos este hábito?
El operador ?? es un recurso nuevo, que se introdujo hace poco tiempo, el año pasado. Además, cuando estamos trabajando con una función muy grande y necesitamos usar los valores en medio de ella, puede ser difícil definir esta información como valores predeterminados en la declaración de los parámetros.
¿Por qué deberíamos evitar este hábito?
El operador ??, a diferencia del operador ||, solo se activa en los valores null y undefined, y no en cualquier otro valor que podría ser identificado como false. Además, si sus funciones son tan largas que no puede definir valores predeterminados en la declaración de sus parámetros, dividirlas o “romperlas” en funciones más pequeñas puede ser una buena idea.
3 - Usar el tipo any como tipo de variables
Cómo puede estar el código
Puede que decidamos usar el tipo any en variables de las que no estamos seguros sobre el formato de su información, como una variable que espera el retorno de datos de una API:
async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: any = await response.json()
return products
}
Cómo debería estar el código
En la mayoría de los casos en que definimos una variable con el tipo any, deberíamos reemplazarlo con el tipo unknown:
async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: unknown = await response.json()
return products as Product[]
}
¿Por qué tenemos este hábito?
El tipo any es muy conveniente, ya que deshabilita todas las verificaciones de tipos, lo que evita errores en la compilación de TypeScript. Además, frecuentemente el tipo any se utiliza incluso en tipificaciones oficiales (por ejemplo, response.json() en el ejemplo anterior está tipado como Promise
¿Por qué deberíamos evitar este hábito?
Este tipo deshabilita básicamente todas las verificaciones de tipo. Cualquier variable que se cree con el tipo any será ignorada por la verificación de tipo. Esto hace que sea muy difícil encontrar errores, ya que el código fallará solo cuando nuestras suposiciones sobre la estructura de los tipos sean relevantes para el compilador en tiempo de ejecución.
4 - Definir el retorno de un valor con as
Cómo puede estar el código
El código puede estar informando al compilador un tipo que no puede deducir (inferir):
async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: unknown = await response.json()
return products as Product[]
}
Cómo debería estar el código
En este caso se pueden usar type guards:
function isArrayOfProducts (obj: unknown): obj is Product[] {
return Array.isArray(obj) obj.every(isProduct)
}
function isProduct (obj: unknown): obj is Product {
return obj != null
typeof (obj as Product).id === 'string'
}
async function loadProducts(): Promise<Product[]> {
const response = await fetch('https://api.mysite.com/products')
const products: unknown = await response.json()
if (!isArrayOfProducts(products)) {
throw new TypeError('Received malformed products API response')
}
return products
}
¿Por qué tenemos este hábito?
Cuando convertimos un código JavaScript a TypeScript, la base de código existente generalmente realiza suposiciones sobre tipos que no pueden ser automáticamente identificadas por el compilador TypeScript. En estos casos, informar un valor diferente a través de as OtroTipo puede aumentar la velocidad de la conversión sin necesidad de que cambiemos la configuración en el archivo tsconfig.json.
¿Por qué deberíamos evitar este hábito?
Aunque la definición de tipos puede guardarse en el momento, puede cambiar cuando alguien mueva el código. Los type guards garantizarán que todas las verificaciones de tipo sean explícitas.
5 - Usar as any en pruebas
Cómo puede estar el código
Puede ocurrir que creemos una estructura incompleta para las pruebas de nuestro código:
interface User {
id: string
firstName: string
lastName: string
email: string
}
test('createEmailText returns text that greats the user by first name', () => {
const user: User = {
firstName: 'John'
} as any
expect(createEmailText(user)).toContain(user.firstName)
}
Cómo debería estar el código
Si es necesario simular datos para nuestras pruebas, es mejor mover la lógica de simulación cerca de la estructura que queremos simular y hacerla reutilizable:
interface User {
id: string
firstName: string
lastName: string
email: string
}
class MockUser implements User {
id = 'id'
firstName = 'John'
lastName = 'Doe'
email = '[email protected]'
}
test('createEmailText returns text that greats the user by first name', () => {
const user = new MockUser()
expect(createEmailText(user)).toContain(user.firstName)
}
¿Por qué tenemos este hábito?
Cuando escribimos pruebas en una base de código que aún no tiene una gran cobertura de pruebas, generalmente encontramos grandes estructuras de datos, pero solo partes de ellas son necesarias para la funcionalidad específica que se está probando. Así, no necesitamos preocuparnos por las otras propiedades a corto plazo.
¿Por qué deberíamos evitar este hábito?
Abandonar la creación de una simulación de datos nos afectará en el futuro, cuando una de las propiedades de nuestra estructura cambie y necesitemos realizar un cambio en todas las pruebas, en lugar de en un solo lugar. Esto puede ser un inconveniente. Además, habrá situaciones en las que el código probado dependerá de propiedades que no consideramos importantes antes y, por eso, será necesario cambiar todas las pruebas de esta funcionalidad.
6 - Propiedades opcionales
Cómo puede estar el código
Puede que creemos propiedades como opcionales que existen en algunos casos, y en otros no:
interface Product {
id: string
type: 'digital' | 'physical'
weightInKg?: number
sizeInMb?: number
}
Cómo debería estar el código
Debemos definir explícitamente las combinaciones que existen y las que no existen:
interface Product {
id: string
type: 'digital' | 'physical'
}
interface DigitalProduct extends Product {
type: 'digital'
sizeInMb: number
}
interface PhysicalProduct extends Product {
type: 'physical'
weightInKg: number
}
¿Por qué tenemos este hábito?
Definir propiedades como opcionales en lugar de separar los tipos en más interfaces es algo más fácil y también resulta en menos código. Esto también requiere un conocimiento más profundo del producto que se está creando y puede limitar el uso del código si las suposiciones sobre el producto cambian.
¿Por qué deberíamos evitar este hábito?
El gran beneficio de los sistemas de tipos es que pueden sustituir las verificaciones en tiempo de ejecución por las verificaciones en tiempo de compilación. Al definir tipos de manera más explícita, es posible acceder a verificaciones en tiempo de compilación para errores que de otra manera podrían pasar desapercibidos. Por ejemplo, garantizar que toda interfaz DigitalProduct tiene una propiedad sizeInMb.
7 - Nombrar generics con solo una letra
Cómo puede estar el código
Quizás tengamos el hábito de crear generics usando solo una letra:
function head<T> (arr: T[]): T | undefined {
return arr[0]
}
Cómo debería estar el código
Es mejor definir un nombre que describa por completo el contexto del generic:
function head<Element> (arr: Element[]): Element | undefined {
return arr[0]
}
¿Por qué tenemos este hábito?
Este hábito creció porque incluso la documentación oficial de TypeScript usa una letra para crear generics. Además, es más rápido y no necesitamos pensar tanto para solo presionar la letra T que escribir un nombre completo.
¿Por qué deberíamos evitar este hábito?
Las variables de tipo en generics son variables como cualquier otra. Abandonamos la idea de describir los detalles técnicos de las variables en sus nombres cuando las IDEs comenzaron a mostrar esos detalles. Por ejemplo, en lugar de escribir const strName = 'Daniel' solo escribimos const name = 'Daniel'. Además, las variables creadas con solo una letra son desincentivadas, ya que puede ser difícil entender su propósito sin mirar su declaración.
8 - Realizar la verificación de booleanos no booleanos
Cómo puede estar el código
Puede que tengamos el hábito de verificar si una variable está definida solo informando su valor en una declaración if():
function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}
Cómo debería estar el código
Debemos informar explícitamente la condición que estamos esperando:
function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages !== undefined) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}
¿Por qué tenemos este hábito?
Escribir la condición que deseamos realizar de manera resumida es más sucinto y nos ayuda a evitar pensar sobre lo que realmente deseamos verificar.
¿Por qué deberíamos evitar este hábito?
Quizás deberíamos pensar sobre lo que realmente queremos verificar. Los ejemplos anteriores, por ejemplo, tratan el caso de que la variable countOfNewMessages sea 0 de manera diferente.
9 - El operador “Bang Bang”
Cómo puede estar el código
Puede que intentemos convertir un valor no booleano a booleano:
function createNewMessagesResponse (countOfNewMessages?: number) {
if (!!countOfNewMessages) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}
Cómo debería estar el código
Debemos informar explícitamente la condición que estamos esperando:
function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages !== undefined) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}
¿Por qué tenemos este hábito?
En opinión de algunas personas, solo podemos decir que realmente estamos comenzando con JavaScript cuando logramos entender el operador !!. Es muy corto y sucinto, y si ya lo has usado algunas veces o estás acostumbrado a usarlo, sabes por experiencia propia que puede ser muy útil. Es un atajo que nos permite convertir cualquier valor a booleano. Esto puede ser útil especialmente en códigos donde no hay una separación clara entre los valores que pueden ser considerados falsos, como null, undefined y “”.
¿Por qué deberíamos evitar este hábito?
Así como muchos otros atajos, usar !! oscurece el verdadero objetivo del código, lo que obliga a un mayor conocimiento interno para entenderlo. Esto hace que la base de código sea menos accesible para nuevos programadores, ya sean nuevos en la programación en general o solo nuevos en el lenguaje JavaScript. Además, este hábito puede insertar sutilmente errores en nuestro código. El problema con la variable countOfNewMessages siendo 0 que mostramos en el último tema continúa con el !!.
10 - != null
Cómo puede estar el código
La estructura != null, la hermana menor del Operador “Bang Bang”, nos permite verificar valores null y undefined al mismo tiempo:
function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages != null) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}
Cómo debería estar el código
Debemos informar explícitamente la condición que estamos esperando:
function createNewMessagesResponse (countOfNewMessages?: number) {
if (countOfNewMessages !== undefined) {
return `You have ${countOfNewMessages} new messages`
}
return 'Error: Could not retrieve number of new messages'
}
¿Por qué tenemos este hábito?
Si has llegado hasta aquí, tu base de código y habilidad de desarrollador con seguridad ya están en un buen formato. Incluso la mayoría de las herramientas de linting que refuerzan el uso del operador !== en lugar de != hacen una excepción para la estructura != null. Si no hay una distinción clara en el código sobre la diferencia entre null y undefined, usar != null nos ayuda a verificar las dos posibilidades de una manera más corta.
¿Por qué deberíamos evitar este hábito?
Aunque los valores nulos han sido un inconveniente en los primeros días de JavaScript, con el uso de TypeScript en modo estricto, estos valores pueden convertirse en un miembro valioso del cinturón de utilidades del lenguaje. Un patrón que he visto es definir los valores nulos como cosas que no existen, y undefined como valores desconocidos. Por ejemplo, user.firstName === null puede significar que el usuario literalmente no tiene un primer nombre, mientras que user.firstName === undefined significa solo que no le hemos preguntado eso al usuario aún (y user.firstName === “” podría significar que el primer nombre es literalmente una cadena vacía).
Conclusión
En este artículo hemos visto algunas prácticas que podemos haber desarrollado con el tiempo y que pueden ser malas. Cuando entendemos en qué situaciones pueden perjudicarnos sutilmente, es más fácil aceptar el cambio que necesitamos hacer.
Sabemos que programar es un constante perfeccionamiento de nuestras técnicas y al prestar atención a ellas nos convertimos en programadores cada vez mejores. Y si deseas aprender o perfeccionar tu TypeScript, no dejes de inscribirte en nuestro curso Dominando TypeScript, donde enseñamos todos los conceptos de este increíble superset ?.
¡Hasta el próximo artículo!