Saltar al contenido principal

📘 Manual de Prácticas en JavaScript y TypeScript

Estas son las convenciones que vamos a utilizar para nuestro código en TypeScript.

Parte 1: Fundamentos y Estilo

1. Convenciones de Estilo y Formateo

  • Indentación: Utiliza 2 espacios por nivel de indentación.
  • Comillas: Prefiere comillas simples ' para strings.
  • Punto y coma: Incluye punto y coma al final de cada sentencia.
  • Nombres:
    • Variables y funciones: camelCase.
    • Clases y tipos: PascalCase.
    • Constantes: UPPER_CASE.

Estas convenciones mejoran la legibilidad y consistencia del código.

2. Declaraciones de Variables

  • Utiliza const por defecto. Solo usa let si necesitas reasignar la variable.
  • Evita el uso de var; puede causar problemas de hoisting y ámbito.
typescript
CopiarEditar
const MAX_USERS = 100;
let currentUsers = 0;

const y let proporcionan un ámbito de bloque, lo que reduce errores relacionados con el ámbito de las variables.

3. Estructuras de Control y Flujo

  • Evita anidar múltiples niveles de condicionales; utiliza retornos tempranos.
  • Prefiere switch en lugar de múltiples if-else cuando sea apropiado.
typescript
CopiarEditar
function getUserRole(role: string): string {
switch (role) {
case 'admin':
return 'Administrador';
case 'user':
return 'Usuario';
default:
return 'Invitado';
}
}

Un flujo de control claro facilita el mantenimiento y la comprensión del código.


Parte 2: JavaScript Moderno

4. Funciones Puras y Arrow Functions

  • Prefiere funciones puras: sin efectos secundarios y que devuelvan el mismo resultado para los mismos argumentos.
  • Utiliza funciones flecha para funciones anónimas o callbacks.
typescript
CopiarEditar
const add = (a: number, b: number): number => a + b;

Las funciones puras son más fáciles de testear y razonar.

5. Manejo de this y Ámbitos

  • Evita el uso de this fuera de clases; puede ser confuso.
  • Utiliza funciones flecha para mantener el contexto de this.
typescript
CopiarEditar
class Counter {
count = 0;

increment = (): void => {
this.count++;
};
}

Las funciones flecha no tienen su propio this, lo que ayuda a mantener el contexto adecuado.

6. Módulos (ESM vs CommonJS)

  • Utiliza módulos ES6 (import/export) en lugar de CommonJS (require/module.exports).
typescript
CopiarEditar
// ES6 Module
import { myFunction } from './myModule';
export const myVar = 42;

Los módulos ES6 son el estándar moderno y ofrecen mejor compatibilidad con herramientas actuales.

7. Promesas y Async/Await

  • Utiliza async/await para manejar código asíncrono de manera más legible.
  • Maneja errores con try/catch.
typescript
CopiarEditar
async function fetchData(url: string): Promise<void> {
try {
const response = await fetch(url);
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error al obtener datos:', error);
}
}

async/await simplifica el manejo de operaciones asíncronas y mejora la legibilidad del código.


Parte 3: TypeScript Aplicado

8. Tipado Estático y Dinámico

  • Aprovecha el tipado estático de TypeScript para detectar errores en tiempo de compilación.
  • Evita el uso de any; si necesitas un tipo flexible, utiliza unknown y realiza comprobaciones de tipo.
typescript
CopiarEditar
function processValue(value: unknown): void {
if (typeof value === 'string') {
console.log(value.toUpperCase());
}
}

unknown obliga a realizar comprobaciones de tipo, lo que mejora la seguridad del código.

9. Interfaces, Tipos y Enums

  • Utiliza interface para definir la forma de objetos.
  • Utiliza type para alias de tipos o combinaciones.
  • Utiliza enum para conjuntos de valores constantes.
typescript
CopiarEditar
interface User {
id: number;
name: string;
}

type ID = number | string;

enum Role {
Admin = 'ADMIN',
User = 'USER',
}

Estas estructuras proporcionan claridad y seguridad en el manejo de datos.

10. Tipos Genéricos

  • Utiliza genéricos para crear funciones y clases reutilizables y seguras.
typescript
CopiarEditar
function identity<T>(value: T): T {
return value;
}

Los genéricos permiten escribir código flexible sin perder el beneficio del tipado estático.

11. Narrowing, Type Guards y Assertions

  • Utiliza técnicas de narrowing para refinar tipos en tiempo de ejecución.
  • Utiliza type guards para comprobar tipos específicos.
typescript
CopiarEditar
function isString(value: unknown): value is string {
return typeof value === 'string';
}

Estas técnicas mejoran la seguridad y robustez del código al manejar diferentes tipos.

Ejemplo 1: typeof narrowing

ts
CopiarEditar
function printLength(value: string | string[]) {
if (typeof value === 'string') {
console.log(value.length); // Aquí TypeScript sabe que es string
} else {
console.log(value.length); // Aquí es string[]
}
}

Ejemplo 2: in operator

ts
CopiarEditar
type Dog = { bark: () => void };
type Cat = { meow: () => void };

function makeSound(animal: Dog | Cat) {
if ('bark' in animal) {
animal.bark();
} else {
animal.meow();
}
}


Type Guards personalizados

Puedes crear funciones que actúan como filtros de tipo, usando el operador is:

ts
CopiarEditar
function isString(value: unknown): value is string {
return typeof value === 'string';
}

function printUpper(value: unknown) {
if (isString(value)) {
console.log(value.toUpperCase());
}
}

Type Guards permiten encapsular lógica de validación, y el compilador actúa en consecuencia.


🧱 Type Assertions

Cuando sabes con certeza qué tipo tiene una variable, puedes usar "type assertions":

ts
CopiarEditar
const element = document.getElementById('myInput') as HTMLInputElement;
console.log(element.value);

Advertencia: los type assertions (as Type) no validan el valor, solo le dicen al compilador que confíe en ti.

12. Utilización de Tipos Utilitarios

  • Aprovecha los tipos utilitarios de TypeScript como Partial, Pick, Omit, etc.
typescript
CopiarEditar
type User = {
id: number;
name: string;
email: string;
};

type UserPreview = Pick<User, 'id' | 'name'>;

Los tipos utilitarios ayudan a manipular y transformar tipos de manera eficiente.

Partial<T>

Convierte todas las propiedades de T en opcionales.

ts
CopiarEditar
interface User {
id: number;
name: string;
email: string;
}

type UserUpdate = Partial<User>;

ts
CopiarEditar
const updateUser: UserUpdate = {
name: 'Nuevo Nombre',
}; // válido, aunque falten `id` y `email`

Útil en formularios de edición o PATCH requests donde no todos los campos son requeridos.


Pick<T, K>

Crea un nuevo tipo seleccionando solo algunas propiedades de T.

ts
CopiarEditar
type UserPreview = Pick<User, 'id' | 'name'>;

const preview: UserPreview = {
id: 1,
name: 'Ana',
};

Ideal cuando necesitas solo una parte de un objeto (ej. mostrar resumen de usuario).


Omit<T, K>

Crea un nuevo tipo excluyendo propiedades específicas de T.

ts
CopiarEditar
type UserWithoutEmail = Omit<User, 'email'>;

const noEmail: UserWithoutEmail = {
id: 1,
name: 'Luis',
};

Muy útil al exponer datos al cliente donde no quieres enviar campos como password, email, etc.


Parte 4: Diseño y Arquitectura

13. Principios SOLID

  • Aplica los principios SOLID para mejorar la calidad y mantenibilidad del código.

Estos principios fomentan un diseño de software más limpio y escalable.

14. Organización del Código y Modularidad

  • Divide el código en módulos y archivos según su responsabilidad.
  • Evita archivos grandes y funciones monolíticas.

Una buena organización facilita el mantenimiento y la escalabilidad del proyecto.

Objetivo

Tener un sistema mantenible, predecible y escalable. El objetivo es evitar "spaghetti code", reducir el acoplamiento y fomentar la reutilización.


Principios Clave

  • Separación de responsabilidades: cada archivo debe tener un propósito claro.
  • Alta cohesión, bajo acoplamiento: agrupar código relacionado, evitar dependencias cruzadas innecesarias.
  • Single Responsibility Principle: cada módulo debe encargarse de una sola cosa.

Estructura típica en apps TypeScript

bash
CopiarEditar
src/
├── modules/ # Agrupación por feature (preferido)
│ ├── auth/
│ │ ├── auth.service.ts
│ │ ├── auth.controller.ts
│ │ └── auth.types.ts
│ └── user/
├── shared/ # Utilidades, tipos comunes
├── config/ # Configs globales
├── lib/ # Funciones puras, lógica reusable
├── tests/

Esta estructura es feature-based, lo que ayuda a escalar horizontalmente sin colapsar la arquitectura.


🧪 Mal ejemplo

ts
CopiarEditar
// userService.ts
export function register(user) { /* lógica */ }
export function login(user) { /* lógica */ }
export function update(user) { /* lógica */ }

Esto escala mal: archivo con demasiadas responsabilidades.


Buen ejemplo (modular y cohesivo)

ts
CopiarEditar
// modules/user/registerUser.ts
export function registerUser(data: RegisterDto) { /* lógica */ }

// modules/user/updateUser.ts
export function updateUser(data: UpdateDto) { /* lógica */ }

Facilita testing, refactor y colaboración en equipos grandes.

15. Patrones de Diseño Comunes

  • Aplica patrones como Factory, Singleton y Strategy donde sea apropiado.

Los patrones de diseño proporcionan soluciones probadas a problemas comunes de diseño de software.

Objetivo

Aplicar soluciones estructuradas y probadas para problemas comunes de desarrollo. Los patrones aumentan la expresividad, flexibilidad y escalabilidad de tu base de código.


1. Factory

Crea objetos complejos sin exponer la lógica de construcción.

ts
CopiarEditar
interface User {
id: number;
role: 'admin' | 'user';
}

function createUser(role: 'admin' | 'user'): User {
return {
id: Math.random(),
role,
};
}

Útil para construir objetos testables, configurables o con lógica condicional.


2. Singleton

Una única instancia compartida por toda la app.

ts
CopiarEditar
class Logger {
private static instance: Logger;
private constructor() {}

static getInstance(): Logger {
if (!Logger.instance) Logger.instance = new Logger();
return Logger.instance;
}

log(msg: string) {
console.log(msg);
}
}

const logger = Logger.getInstance();

Útil para servicios como logging, cache o configuración.


3. Strategy

Permite cambiar la lógica de una función sin modificar su estructura.

ts
CopiarEditar
type Strategy = (a: number, b: number) => number;

function calculate(a: number, b: number, strategy: Strategy) {
return strategy(a, b);
}

const add: Strategy = (a, b) => a + b;
const multiply: Strategy = (a, b) => a * b;

calculate(2, 3, add); // 5
calculate(2, 3, multiply); // 6

Útil para algoritmos dinámicos, validadores, procesadores de datos.


¿Cuándo no usar patrones?

Cuando la abstracción no aporta legibilidad o está sobreingenierizada. Un patrón mal aplicado complica más que ayuda.

16. Testing y Tipado de Mocks

  • Escribe pruebas unitarias y de integración utilizando herramientas como Jest.
  • Utiliza mocks tipados para simular dependencias.

Las pruebas aseguran el correcto funcionamiento del código y facilitan el refactorizado.

Objetivo

Escribir pruebas robustas y confiables que no se rompan con cambios triviales y verifiquen el comportamiento real, no solo la implementación.


Herramientas recomendadas

TipoHerramienta
Unit TestingJest
IntegrationSupertest, Vitest
E2EPlaywright, Cypress
Mocksjest.fn(), ts-mockito

🧠 Principios clave

  • Escribe unit tests por cada función pura.
  • Usa integration tests para testear flujos entre servicios o módulos.
  • En aplicaciones fullstack, escribe E2E tests con Playwright o Cypress.

Buen ejemplo: testear servicio con mock tipado

ts
CopiarEditar
// userService.ts
export class UserService {
constructor(private emailService: EmailService) {}

register(email: string) {
this.emailService.send(email, 'Welcome!');
return true;
}
}

ts
CopiarEditar
// userService.test.ts
import { UserService } from './userService';

const emailServiceMock = {
send: jest.fn(),
};

const service = new UserService(emailServiceMock);

test('should send welcome email', () => {
service.register('test@mail.com');
expect(emailServiceMock.send).toHaveBeenCalledWith('test@mail.com', 'Welcome!');
});

Se mockea el servicio externo pero se testea el comportamiento del servicio principal.


Tipado fuerte en mocks (TypeScript)

ts
CopiarEditar
type EmailService = {
send: (email: string, body: string) => void;
};

const mockEmailService: jest.Mocked<EmailService> = {
send: jest.fn(),
};

Esto asegura que tu mock cumpla con el contrato real del servicio.


Consejos adicionales

  • Nunca testees solo “que una función fue llamada”. También testea el output real.
  • Organiza tus tests en carpetas por feature, no por tipo de test.
  • Asegúrate de que los mocks no oculten errores en el código real.

Parte 5: Rendimiento y Seguridad

17. Rendimiento

  • Evita operaciones costosas dentro de bucles.
  • Utiliza técnicas como memoización y lazy loading cuando sea apropiado.

Un código eficiente mejora la experiencia del usuario y reduce el consumo de recursos.

Objetivo

Optimizar el uso de CPU, memoria, y minimizar bloqueos del event loop. Esto mejora la experiencia de usuario, reduce el TTI (Time to Interactive) y aumenta la escalabilidad.


Fundamentos Clave

  • JavaScript corre en un solo hilo (thread) → operaciones pesadas bloquean la UI.
  • Cada await o Promise cede el control del hilo → aprovechamiento de asincronía.
  • El garbage collector (GC) libera memoria automáticamente, pero mal manejo de referencias causa leaks.

Buenas Prácticas

1. Evita operaciones costosas dentro del render

tsx
CopiarEditar
// ❌ Mal
const renderList = () => {
const sortedItems = items.sort((a, b) => a.createdAt - b.createdAt);
return sortedItems.map(item => <li key={item.id}>{item.name}</li>);
};

// ✅ Bien — calcular fuera del render
const sortedItems = useMemo(() => {
return [...items].sort((a, b) => a.createdAt - b.createdAt);
}, [items]);

Usar useMemo evita recalcular en cada render, ahorrando CPU.


2. Usa debouncing/throttling en inputs intensivos

ts
CopiarEditar
const debouncedSearch = useCallback(debounce((q) => search(q), 300), []);

Reduce llamadas innecesarias a APIs o renders por cada keystroke.


3. Lazy loading en componentes pesados

ts
CopiarEditar
const Chart = dynamic(() => import('./HeavyChart'), { ssr: false });

Optimiza el bundle inicial, carga el módulo solo cuando se necesita.


4. Evita loops innecesarios y duplicación de lógica

ts
CopiarEditar
// ❌ No hagas doble mapeo
data.filter(f).map(m);

// ✅ Hacelo en un solo paso
data.reduce((acc, item) => {
if (f(item)) acc.push(m(item));
return acc;
}, []);

18. Seguridad en Tiempo de Ejecución

  • Evita el uso de eval y otras prácticas inseguras.
  • Valida y sanitiza las entradas del usuario.

La seguridad es crucial para proteger la aplicación y los datos de los usuarios.

🎯 Objetivo

Evitar que tu código introduzca vulnerabilidades como inyecciones, acceso no autorizado o manipulación del prototipo, tanto en cliente como en servidor.


Amenazas comunes en JS

1. eval() es el infierno

ts
CopiarEditar
// ❌ Malísimo
eval("console.log(userInput)");

// ✅ Nunca uses eval. Usa funciones seguras como JSON.parse o lógica declarativa.

eval ejecuta código arbitrario y abre puertas a inyecciones.


2. Prototype Pollution

ts
CopiarEditar
const user = {};
Object.prototype.admin = true;

console.log(user.admin); // true

Asegúrate de sanitizar entradas antes de asignarlas a objetos. Usa Object.create(null) si querés evitar prototipos heredados.


3. Validar siempre la entrada del usuario

ts
CopiarEditar
// ❌ Inseguro
function login(req) {
const { email, password } = req.body;
}

// ✅ Seguro
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
});

Sanitiza todo input. Usa zod, yup o validadores equivalentes en backend.


4. Cookies: HttpOnly, Secure, SameSite

ts
CopiarEditar
res.setHeader('Set-Cookie', `token=${jwt}; HttpOnly; Secure; SameSite=Strict`);

Esto evita ataques XSS y CSRF en apps web.

19. Buenas Prácticas en Entornos Frontend y Backend

  • En el frontend, evita exponer información sensible y maneja adecuadamente los errores.
  • En el backend, protege las rutas y controla los accesos.

Adaptar las buenas prácticas al entorno específico mejora la eficacia y seguridad de la aplicación.

En Frontend

Lo que debes hacer

  • Usar Content-Security-Policy (CSP) para evitar inyecciones.
  • Nunca exponer secretos en variables públicas (NEXT_PUBLIC_API_KEY puede ser visto por cualquiera).
  • Minificar y obfuscar si vas a producción.
  • Usa next/image, lazy loading, prefetch de rutas.
  • Deshabilita devtools en producción si la lógica crítica está en el cliente (aunque no es seguro al 100%).

Lo que debes evitar

  • Evitar el uso de localStorage para tokens sin cifrado → vulnerable a XSS.
  • No dejar logs (console.log, debugger) en producción.
  • No dejar librerías pesadas (moment.js, lodash completo) si se pueden tree-shakear o reemplazar.

En Backend (Node.js, NestJS, Express)

Lo que debes hacer

  • Habilitar Helmet: seguridad por cabeceras.
  • Limitar tamaño de payload: body-parser limit.
  • Configurar Rate limiting (express-rate-limit, nestjs/throttler).
  • Validar todas las entradas con class-validator, zod o Joi.
  • Controlar errores internos: nunca devolver stack trace al cliente.

Lo que debes evitar

  • Nunca interpolar directamente inputs en queries SQL o Mongo → SQL Injection.
  • No exponer versiones del servidor (x-powered-by: Express).
  • No escribir archivos en disco directamente con datos del cliente sin validación.

Parte 6: Herramientas y Ecosistema

20. Configuración de Proyectos TypeScript

  • Configura adecuadamente el archivo tsconfig.json para aprovechar al máximo TypeScript.
json
CopiarEditar
{
"compilerOptions": {
"target": "ES6",
"module": "ES6",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true}
}

Una configuración adecuada mejora la calidad del código y la experiencia de desarrollo.

21. Integración con Bundlers y Linters

  • Utiliza herramientas como Webpack, ESLint y Prettier para mejorar el flujo de trabajo y mantener la calidad del código.

Estas herramientas automatizan tareas y ayudan a mantener un código limpio y consistente.

22. Documentación con JSDoc y TypeDoc

  • Documenta el código utilizando JSDoc y genera documentación automática con TypeDoc.
typescript
CopiarEditar
/**
* Calcula la suma de dos números.
* @param a - Primer número.
* @param b - Segundo número.
* @returns La suma de a y b.
*/
function sum(a: number, b: number): number {
return a + b;
}

Una buena documentación facilita la comprensión y uso del código por parte de otros desarrolladores.

23. Diferencias entre JSDoc/TypeDoc y Swagger

CaracterísticaJSDoc / TypeDocSwagger (OpenAPI)
🧠 PropósitoDocumentar código TypeScript o JSDocumentar y definir una API HTTP REST
📚 OutputDocumentación técnica del código fuenteEspecificación de endpoints y respuestas
🔍 Dónde se usaFunciones, clases, interfacesRutas HTTP, request/response, status codes
⚙️ HerramientasTypeDoc, VSCode tooltipsSwagger UI, OpenAPI Generator
🚀 Utilidad principalComprensión por parte de desarrolladoresInteroperabilidad API / Cliente-Servidor

Ejemplo con JSDoc:

ts
CopiarEditar
/**
* Suma dos números.
* @param a Primer número
* @param b Segundo número
* @returns La suma de ambos
*/
function sum(a: number, b: number): number {
return a + b;
}

Ejemplo con Swagger (NestJS)

ts
CopiarEditar
@ApiResponse({ status: 200, description: 'Usuario encontrado' })
@Get(':id')
getUser(@Param('id') id: string): Promise<User> {
return this.userService.findOne(id);
}

Resumen:

  • Usa JSDoc/TypeDoc para documentar el código fuente y ayudar a los desarrolladores.
  • Usa Swagger/OpenAPI para definir y comunicar el contrato de una API con otros sistemas o clientes.

Ejemplo: Servicio y Endpoint en NestJS con JSDoc + Swagger

ts
CopiarEditar
import { Controller, Get, Param } from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';

interface User {
id: number;
name: string;
email: string;
}

/**
* Servicio que maneja operaciones relacionadas con usuarios.
*/
export class UserService {
/**
* Busca un usuario por su ID.
* @param id - ID del usuario
* @returns Un objeto de tipo `User` si se encuentra
*/
findOne(id: number): User {
return {
id,
name: 'Juan Pérez',
email: 'juan@example.com',
};
}
}

@ApiTags('Users') // Swagger
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}

/**
* Obtiene un usuario por ID.
* @param id - El ID numérico del usuario
* @returns Un usuario con nombre y correo electrónico
*/
@Get(':id')
@ApiOperation({ summary: 'Obtener un usuario por ID' }) // Swagger
@ApiResponse({ status: 200, description: 'Usuario encontrado con éxito' }) // Swagger
@ApiResponse({ status: 404, description: 'Usuario no encontrado' }) // Swagger
getUserById(@Param('id') id: string): User {
return this.userService.findOne(+id);
}
}