📘 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.
- Variables y funciones:
Estas convenciones mejoran la legibilidad y consistencia del código.
2. Declaraciones de Variables
- Utiliza
constpor defecto. Solo usaletsi 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
switchen lugar de múltiplesif-elsecuando 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
thisfuera 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/awaitpara 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, utilizaunknowny 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
interfacepara definir la forma de objetos. - Utiliza
typepara alias de tipos o combinaciones. - Utiliza
enumpara 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
| Tipo | Herramienta |
|---|---|
| Unit Testing | Jest |
| Integration | Supertest, Vitest |
| E2E | Playwright, Cypress |
| Mocks | jest.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
PlaywrightoCypress.
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
awaitoPromisecede 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
evaly 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_KEYpuede ser visto por cualquiera). - Minificar y obfuscar si vas a producción.
- Usa
next/image,lazy loading, prefetch de rutas. - Deshabilita
devtoolsen 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
localStoragepara 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,zodoJoi. - 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.jsonpara 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ística | JSDoc / TypeDoc | Swagger (OpenAPI) |
|---|---|---|
| 🧠 Propósito | Documentar código TypeScript o JS | Documentar y definir una API HTTP REST |
| 📚 Output | Documentación técnica del código fuente | Especificación de endpoints y respuestas |
| 🔍 Dónde se usa | Funciones, clases, interfaces | Rutas HTTP, request/response, status codes |
| ⚙️ Herramientas | TypeDoc, VSCode tooltips | Swagger UI, OpenAPI Generator |
| 🚀 Utilidad principal | Comprensión por parte de desarrolladores | Interoperabilidad 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);
}
}