TypeScript es un lenguaje de programación de código abierto ampliamente utilizado que es perfecto para el desarrollo moderno.

Con su avanzado sistema de tipos, TypeScript permite a los desarrolladores escribir código más robusto, mantenible y escalable. Pero, para aprovechar realmente el poder de TypeScript y construir proyectos de alta calidad, es esencial entender y seguir las mejores prácticas.

En este artículo, nos adentraremos en el mundo de TypeScript y exploraremos las 20 mejores prácticas para dominar el lenguaje. Estas mejores prácticas cubren una amplia gama de temas y proporcionan ejemplos concretos de cómo aplicarlos en proyectos del mundo real. Tanto si estás empezando como si eres un desarrollador experimentado de TypeScript, este artículo te proporcionará valiosas ideas y consejos que te ayudarán a escribir código limpio y eficiente.

Así que, coge una taza de café ☕️, ¡y empecemos nuestro viaje para dominar TypeScript! ✌🏻

Buena práctica 1: Comprobación estricta de tipos


Empezaremos por las más básicas. Imagina ser capaz de detectar errores potenciales incluso antes de que ocurran, ¿suena demasiado bueno para ser verdad? Bueno, eso es exactamente lo que la comprobación estricta de tipos en TypeScript puede hacer por ti.

Esta mejor práctica consiste en detectar esos errores furtivos que pueden colarse en tu código y causarte dolores de cabeza.

La comprobación estricta de tipos consiste en asegurarse de que los tipos de tus variables coinciden con los tipos que esperas que sean. Esto significa que si declaras que una variable es de tipo cadena, TypeScript se asegurará de que el valor asignado a esa variable sea efectivamente una cadena y no un número, por ejemplo. Esto te ayuda a detectar errores desde el principio y a asegurarte de que tu código funciona según lo previsto.

Activar la comprobación estricta de tipos es tan sencillo como añadir "strict": true a tu archivo tsconfig.json (tiene que ser true por defecto). Al hacer esto, TypeScript habilitará un conjunto de comprobaciones que detectarán ciertos errores que de otro modo pasarían desapercibidos.

He aquí un ejemplo de cómo la comprobación estricta de tipos puede salvarte de un error común:

let userName: string = "John";
userName = 123; // TypeScript will raise an error because "123" is not a string.

Siguiendo esta práctica recomendada, podrás detectar errores en una fase temprana y asegurarte de que tu código funciona según lo previsto, lo que te ahorrará tiempo y frustraciones a largo plazo. 🤩

Buena práctica 2: Inferencia de tipos


TypeScript trata de ser explícito con tus tipos, pero eso no significa que tengas que deletrearlo todo.🫣

La inferencia de tipo es la capacidad del compilador de TypeScript para determinar automáticamente el tipo de una variable basándose en el valor que se le asigna. Esto significa que no tienes que especificar explícitamente el tipo de una variable cada vez que la declaras. En su lugar, el compilador mirará el valor e inferirá el tipo por ti.

Por ejemplo, en el siguiente fragmento de código, TypeScript deducirá automáticamente que el tipo de nombre es una cadena:

let name = "John";

La inferencia de tipos es especialmente útil cuando se trabaja con tipos complejos o cuando se inicializa una variable con un valor devuelto por una función.

Pero recuerda, la inferencia de tipos no es una varita mágica, a veces es mejor ser explícito con los tipos, especialmente cuando se trabaja con tipos complejos o cuando quieres asegurarte de que se utiliza un tipo específico.

Buena práctica 3: Linters


Los linters son herramientas que pueden ayudarte a escribir mejor código aplicando un conjunto de reglas y directrices. Pueden ayudarte a detectar errores potenciales y mejorar la calidad general de tu código.

Hay varios linters disponibles para TypeScript, como TSLint y ESLint, que pueden ayudarte a imponer un estilo de código consistente y a detectar errores potenciales. Estos linters pueden configurarse para comprobar cosas como la falta de punto y coma, variables no utilizadas y otros problemas comunes.

Buena práctica 4: Interfaces


Cuando se trata de escribir código limpio y fácil de mantener, las interfaces son tus mejores amigas. Son como un plano para tus objetos, delineando la estructura y las propiedades de los datos con los que trabajarás.

Una interfaz en TypeScript define un contrato para la forma de un objeto. Especifica las propiedades y métodos que debe tener un objeto de ese tipo, y puede usarse como tipo para una variable.

Esto significa que cuando asignas un objeto a una variable con un tipo de interfaz, TypeScript comprobará que el objeto tiene todas las propiedades y métodos especificados en la interfaz.

Aquí tienes un ejemplo de cómo definir y usar una interfaz en TypeScript:

interface User {
 name: string;
 age: number;
}
let user: User = {name: "John", age: 25};

Las interfaces también facilitan la refactorización del código, ya que garantizan que todos los lugares en los que se utiliza un determinado tipo se actualicen a la vez.

Buena práctica 5: Alias de tipo


TypeScript permite crear tipos personalizados utilizando una característica llamada alias de tipo. La principal diferencia entre las características alias de tipo e interfaz es que alias de tipo crea un nuevo nombre para el tipo, mientras que interfaz crea un nuevo nombre para la forma del objeto.

Por ejemplo, puedes utilizar un alias de tipo para crear un tipo personalizado para un punto en un espacio bidimensional:

type Point = { x: number, y: number };
let point: Point = { x: 0, y: 0 };

Los alias de tipo también pueden utilizarse para crear tipos complejos, como:

type User = { name: string, age: number };
type Admin = { name: string, age: number, privileges: string[] };
type SuperUser = User & Admin;

Buena práctica 6: Uso de tuplas


Las tuplas son una forma de representar una matriz de tamaño fijo de elementos con diferentes tipos. Permiten expresar una colección de valores con un orden y tipos específicos.

Por ejemplo, puedes utilizar una tupla para representar un punto en un espacio 2D:

let point: [number, number] = [1, 2];

También puede utilizar una tupla para representar una colección de varios tipos:

let user: [string, number, boolean] = ["Bob", 25, true];

Una de las principales ventajas de utilizar tuplas es que proporcionan una forma de expresar una relación de tipo específica entre los elementos de la colección.

Además, puede utilizar la asignación de desestructuración para extraer los elementos de una tupla y asignarlos a variables:

let point: [number, number] = [1, 2];
let [x, y] = point;
console.log(x, y);

Buena práctica 7: Utilizar cualquier tipo


A veces, puede que no tengamos toda la información sobre el tipo de una variable, pero aún así necesitamos utilizarla en nuestro código. En estos casos, podemos utilizar el tipo any. Pero, como cualquier herramienta poderosa, el uso de any debe ser usado con precaución y propósito.

Una de las mejores prácticas cuando se utiliza any es limitar su uso a casos específicos en los que el tipo es realmente desconocido, como cuando se trabaja con bibliotecas de terceros o datos generados dinámicamente.

Además, es una buena idea añadir aserciones de tipo o guards de tipo para asegurarse de que la variable se está utilizando correctamente. Y cuando sea posible, intenta limitar el tipo de la variable tanto como puedas.

Por ejemplo:

function logData(data: any) {
    console.log(data);
}

const user = { name: "John", age: 30 };
const numbers = [1, 2, 3];

logData(user); // { name: "John", age: 30 }
logData(numbers); // [1, 2, 3]

Otra buena práctica es evitar el uso de cualquier tipo de retorno de función y argumentos de función, ya que puede debilitar la seguridad de tipo de tu código. En su lugar, puedes utilizar un tipo que es más específico o utilizar un tipo que es más general como desconocido u objeto que todavía proporcionan un cierto nivel de seguridad de tipo.

Buena práctica 8: Uso del tipo desconocido


El tipo desconocido es un tipo potente y restrictivo que se introdujo en TypeScript 3.0. Es un tipo más restrictivo que any y puede ayudarte a prevenir errores de tipo no intencionados.

A diferencia de any, cuando usas el tipo desconocido, TypeScript no te permitirá realizar ninguna operación sobre un valor a menos que primero compruebes su tipo. Esto puede ayudarte a detectar errores de tipo en tiempo de compilación, en lugar de en tiempo de ejecución.

Por ejemplo, puedes utilizar el tipo desconocido para crear una función más segura desde el punto de vista del tipo:

function printValue(value: unknown) {
 if (typeof value === "string") {
   console.log(value);
 } else {
   console.log("Not a string");
 }
}

También puedes utilizar el tipo desconocido para crear variables más seguras:

let value: unknown = "hello";
let str: string = value; // Error: Type 'unknown' is not assignable to type 'string'.

Buena práctica 9: "never"


En TypeScript, never es un tipo especial que representa valores que nunca ocurrirán. Se utiliza para indicar que una función no devolverá normalmente, sino que lanzará un error. Esta es una gran manera de indicar a otros desarrolladores (y al compilador) que una función no puede ser utilizada de ciertas maneras, esto puede ayudar a detectar posibles errores.

Por ejemplo, considera la siguiente función que lanza un error si la entrada es menor que 0:

function divide(numerator: number, denominator: number): number {
 if (denominator === 0) {
 throw new Error("Cannot divide by zero");
 }
 return numerator / denominator;
}

Aquí, la función dividir está declarada para devolver un número, pero si el denominador es cero, lanzará un error. Para indicar que esta función no devolverá normalmente en este caso, puedes utilizar never como tipo de retorno:

function divide(numerator: number, denominator: number): number | never {
 if (denominator === 0) {
   throw new Error("Cannot divide by zero");
 }
   return numerator / denominator;
}

Buena práctica 10: Uso del operador keyof


El operador keyof es una potente característica de TypeScript que permite crear un tipo que representa las claves de un objeto. Puede utilizarse para dejar claro qué propiedades están permitidas para un objeto.

Por ejemplo, puedes utilizar el operador keyof para crear un tipo más legible y fácil de mantener para un objeto:

interface User {
 name: string;
 age: number;
}

type UserKeys = keyof User; // "name" | "age"

También puedes utilizar el operador keyof para crear funciones más seguras que tomen un objeto y una clave como argumentos:

function getValue<T, K extends keyof T>(obj: T, key: K) {
 return obj[key];
}
let user: User = { name: "John", age: 30 };
console.log(getValue(user, "name")); // "John"
console.log(getValue(user, "gender")); // Error: Argument of type '"gender"' is not assignable to parameter of type '"name" | "age"'.

Buena Práctica 11: Uso de Enums


Enums, abreviatura de enumeraciones, son una forma de definir un conjunto de constantes con nombre en TypeScript. Se pueden utilizar para crear un código más legible y mantenible, dando un nombre significativo a un conjunto de valores relacionados.

Por ejemplo, puedes utilizar una enumeración para definir un conjunto de posibles valores de estado para un pedido:

enum OrderStatus {
 Pending,
 Processing,
 Shipped,
 Delivered,
 Cancelled
}

let orderStatus: OrderStatus = OrderStatus.Pending;

Los Enums también pueden tener un conjunto personalizado de valores numéricos o cadenas.

 enum OrderStatus {
 Pending = 1,
 Processing = 2,
 Shipped = 3,
 Delivered = 4,
 Cancelled = 5
}

let orderStatus: OrderStatus = OrderStatus.Pending;

Nombra siempre un enum con la primera letra mayúscula y el nombre tiene que estar en singular, como parte de la convención de nomenclatura.

Buena práctica 12: Uso de espacios de nombres


Los espacios de nombres son una forma de organizar el código y evitar colisiones de nombres. Te permiten crear un contenedor para tu código, donde puedes definir variables, clases, funciones e interfaces.

Por ejemplo, puedes utilizar un espacio de nombres para agrupar todo el código relacionado con una función específica:

namespace OrderModule {
 export class Order { /* … */ }
 export function cancelOrder(order: Order) { /* … */ }
 export function processOrder(order: Order) { /* … */ }
}
let order = new OrderModule.Order();
OrderModule.cancelOrder(order);

También puedes utilizar los espacios de nombres para evitar colisiones de nombres proporcionando un nombre único para su código:

namespace MyCompany.MyModule {
 export class MyClass { /* … */ }
}

let myClass = new MyCompany.MyModule.MyClass();

Es importante tener en cuenta que los espacios de nombres son similares a los módulos, pero se utilizan para organizar el código y evitar colisiones de nombres, mientras que los módulos se utilizan para cargar y ejecutar el código.

Buena práctica 13: Uso de tipos de utilidad


Los tipos de utilidad son una característica incorporada de TypeScript que proporciona un conjunto de tipos predefinidos para ayudarte a escribir código más seguro. Permiten realizar operaciones tipográficas comunes y manipular tipos de una manera más conveniente.

Por ejemplo, puede utilizar el tipo de utilidad Pick para extraer un subconjunto de propiedades de un tipo de objeto:

type User = { name: string, age: number, email: string };
type UserInfo = Pick<User, "name" | "email">;

También puede utilizar el tipo de utilidad Exclude para eliminar propiedades de un tipo de objeto:

type User = { name: string, age: number, email: string };
type UserWithoutAge = Exclude<User, "age">;

Puede utilizar el tipo de utilidad Partial para hacer que todas las propiedades de un tipo sean opcionales:

type User = { name: string, age: number, email: string };
type PartialUser = Partial<User>;

Buena práctica 14: "Readonly" y "ReadonlyArray"


Cuando trabajas con datos en TypeScript, puede que quieras asegurarte de que ciertos valores no pueden ser cambiados. Y ahí es donde entran Readonly y ReadonlyArray.

La palabra clave Readonly se utiliza para hacer que las propiedades de un objeto sean de sólo lectura, lo que significa que no pueden ser modificadas después de ser creadas. Esto puede ser útil cuando se trabaja con valores de configuración o constantes, por ejemplo:

 interface Point {
 x: number;
 y: number;
}
let point: Readonly<Point> = {x: 0, y: 0};
point.x = 1; // TypeScript will raise an error because "point.x" is read-only

ReadonlyArray es similar a Readonly pero para arrays. Hace que un array sea de sólo lectura, y no puede ser modificado después de ser creado.

let numbers: ReadonlyArray<number> = [1, 2, 3];
numbers.push(4); // TypeScript will raise an error because "numbers" is read-only

Buena práctica 15: Guardias de tipo (Type Guards)


Cuando se trabaja con tipos complejos en TypeScript, puede ser difícil hacer un seguimiento de las diferentes posibilidades de una variable. Los type guards son una potente herramienta que puede ayudarte a acotar el tipo de una variable basándose en ciertas condiciones.

He aquí un ejemplo de cómo utilizar un type guard para comprobar si una variable es un número:

function isNumber(x: any): x is number {
 return typeof x === "number";
}
let value = 3;
if (isNumber(value)) {
 value.toFixed(2); // TypeScript knows that "value" is a number because of the type guard
}

Los protectores de tipo también pueden utilizarse con el operador in, el operador typeof y el operador instanceof.

Buena práctica 16: Uso de genéricos


Los genéricos son una potente característica de TypeScript que te permite escribir código que puede trabajar con cualquier tipo, haciéndolo más reutilizable. Los genenerics te permiten escribir una única función, clase o interfaz que puede trabajar con múltiples tipos, sin tener que escribir implementaciones separadas para cada tipo.

Por ejemplo, puedes utilizar una función genérica para crear un array de cualquier tipo:

function createArray<T>(length: number, value: T): Array<T> {
 let result = [];

 for (let i = 0; i < length; i++) {
   result[i] = value;
 }

 return result;
}

let names = createArray<string>(3, "Bob");
let numbers = createArray<number>(3, 0);

También puedes utilizar generics para crear una clase que pueda trabajar con cualquier tipo de datos:

 class GenericNumber<T> {
 zeroValue: T;
 add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

Buena práctica 17: Uso de la palabra clave infer


La palabra clave infer es una potente característica de TypeScript que permite extraer el tipo de una variable en un tipo.

Por ejemplo, puedes utilizar la palabra clave infer para crear un tipo más preciso para una función que devuelve una matriz de un tipo específico:

type ArrayType<T> = T extends (infer U)[] ? U : never;
type MyArray = ArrayType<string[]>; // MyArray is of type string

También puede utilizar la palabra clave infer para crear tipos más precisos para una función que devuelve un objeto con una propiedad específica:

type ObjectType<T> = T extends { [key: string]: infer U } ? U : never;
type MyObject = ObjectType<{ name: string, age: number }>; // MyObject is of type {name:string, age: number}

Buena práctica 18: Uso de tipos condicionales


Los tipos condicionales permiten expresar relaciones de tipos más complejas. Permiten crear nuevos tipos basados en las condiciones de otros tipos.

Por ejemplo, puede utilizar un tipo condicional para extraer el tipo de retorno de una función:

type ReturnType<T> = T extends (…args: any[]) => infer R ? R : any;
type R1 = ReturnType<() => string>; // string
type R2 = ReturnType<() => void>; // void

También puedes utilizar tipos condicionales para extraer las propiedades de un tipo de objeto que cumplan una determinada condición:

type PickProperties<T, U> = { [K in keyof T]: T[K] extends U ? K : never }[keyof T];
type P1 = PickProperties<{ a: number, b: string, c: boolean }, string | number>; // "a" | "b"

Buena práctica 19: Uso de tipos asignados


Los tipos mapeados son una forma de crear nuevos tipos basados en tipos existentes. Permiten crear nuevos tipos aplicando un conjunto de operaciones a las propiedades de un tipo existente.

Por ejemplo, puedes utilizar un tipo mapeado para crear un nuevo tipo que represente la versión de sólo lectura de un tipo existente:

type Readonly<T> = { readonly [P in keyof T]: T[P] };
let obj: { a: number, b: string } = { a: 1, b: "hello" };
let readonlyObj: Readonly<typeof obj> = { a: 1, b: "hello" };

También puedes utilizar un tipo asignado para crear un nuevo tipo que represente la versión opcional de un tipo existente:

type Optional<T> = { [P in keyof T]?: T[P] };
let obj: { a: number, b: string } = { a: 1, b: "hello" };
let optionalObj: Optional<typeof obj> = { a: 1 };

Los tipos mapeados pueden utilizarse de diferentes formas:

  • Para crear nuevos tipos, para añadir o eliminar propiedades de un tipo existente.
  • Para cambiar el tipo de las propiedades de un tipo existente.

Buena práctica 20: Uso de decoradores


Los decoradores son una forma de añadir funcionalidad adicional a una clase, método o propiedad utilizando una sintaxis sencilla. Son una forma de mejorar el comportamiento de una clase sin modificar su implementación.

Por ejemplo, puedes utilizar un decorador para añadir registro a un método:

function logMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
 let originalMethod = descriptor.value;

 descriptor.value = function(…args: any[]) {
 console.log(`Calling ${propertyKey} with args: ${JSON.stringify(args)}`);
 let result = originalMethod.apply(this, args);
 console.log(`Called ${propertyKey}, result: ${result}`);
 return result;
 }
}

class Calculator {
 @logMethod
 add(x: number, y: number): number {
 return x + y;
 }
}

También puedes utilizar decoradores para añadir metadatos a una clase, método o propiedad, que pueden utilizarse en tiempo de ejecución.

function setApiPath(path: string) {
 return function (target: any) {
 target.prototype.apiPath = path;
 }
}

@setApiPath("/users")
class UserService {
 // …
}
console.log(new UserService().apiPath); // "/users"

Conclusión


Tanto si eres un principiante como un desarrollador experimentado de TypeScript, espero que este artículo te haya proporcionado valiosas ideas y consejos para ayudarte a escribir código limpio y eficiente. 👍🏼

Recuerda, las mejores prácticas son directrices, no reglas rígidas. Usa siempre tu propio juicio y sentido común cuando escribas código. Y ten en cuenta que, a medida que TypeScript evoluciona y se introducen nuevas características, las mejores prácticas pueden cambiar, así que mantente actualizado y abierto a aprender cosas nuevas.

Esperamos que este artículo te haya resultado útil y te haya inspirado para convertirte en un mejor desarrollador de TypeScript.

¡Feliz programación!

Fuente

Plataforma de cursos gratis sobre programación