TypeScriptBest PracticesClean CodeArquitectura

TypeScript Avanzado: Del JavaScript Común al Código Type-Safe

Lewis Lopez
TypeScript Avanzado: Del JavaScript Común al Código Type-Safe

En 2019 migré un proyecto bancario de JavaScript a TypeScript. Resultado: 67% menos bugs en producción, 40% menos tiempo en debugging.

TypeScript no es “JavaScript con tipos”. Es un sistema de garantías que te salva de errores que cuestan millones.

Por Qué TypeScript es No Negociable en 2024

Historia Real: El Bug de $80K

Escenario: API de transferencias en JavaScript puro.

// JavaScript - Todo se ve bien... hasta producción
function processTransfer(amount, account) {
  if (account.balance > amount) {
    account.balance = account.balance - amount;
    return { success: true };
  }
  return { success: false };
}

// En runtime:
processTransfer('1000', account); // "100050" 🔥
processTransfer(null, account);    // NaN 🔥
processTransfer('abc', account);   // NaN 🔥

Impacto:

  • 156 transacciones corruptas
  • $80K en conciliaciones manuales
  • 3 días de sistemas caídos
  • Pérdida de confianza del cliente

Con TypeScript:

interface Account {
  id: string;
  balance: number;
  currency: Currency;
}

type Currency = 'USD' | 'EUR' | 'COP';

function processTransfer(
  amount: number,
  account: Account
): TransferResult {
  if (account.balance > amount) {
    account.balance = account.balance - amount;
    return { success: true };
  }
  return { success: false };
}

// ❌ Error en COMPILE TIME, no en producción
processTransfer('1000', account);  // Error: string no es number
processTransfer(null, account);     // Error: null no es number
processTransfer(100, null);         // Error: null no es Account

Los 10 Conceptos de TypeScript que Separan Juniors de Seniors

1. Generics: Código Reutilizable y Type-Safe

// ❌ Sin generics: Duplicación o pérdida de tipos
function getFirstNumber(arr: number[]): number {
  return arr[0];
}

function getFirstString(arr: string[]): string {
  return arr[0];
}

// ✅ Con generics: DRY y type-safe
function getFirst<T>(arr: T[]): T {
  return arr[0];
}

const firstNumber = getFirst([1, 2, 3]);      // Type: number
const firstString = getFirst(['a', 'b']);     // Type: string
const firstUser = getFirst(users);            // Type: User

// Generic constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

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

const user: User = { name: 'Lewis', age: 35 };
const name = getProperty(user, 'name');  // Type: string
const age = getProperty(user, 'age');    // Type: number
// getProperty(user, 'email');           // ❌ Error: 'email' no existe

2. Utility Types: El Poder Oculto de TypeScript

interface Product {
  id: string;
  name: string;
  price: number;
  stock: number;
  createdAt: Date;
  updatedAt: Date;
}

// Partial: Todos los campos opcionales
type UpdateProductDto = Partial<Product>;

function updateProduct(id: string, data: UpdateProductDto) {
  // data puede tener cualquier combinación de campos
}

updateProduct('123', { price: 99.99 }); // ✅ Valid
updateProduct('123', { name: 'New' });  // ✅ Valid

// Pick: Seleccionar campos específicos
type ProductSummary = Pick<Product, 'id' | 'name' | 'price'>;

// Omit: Excluir campos
type CreateProductDto = Omit<Product, 'id' | 'createdAt' | 'updatedAt'>;

// Required: Todos los campos obligatorios
type ProductRequired = Required<Partial<Product>>;

// Readonly: Inmutabilidad
type ImmutableProduct = Readonly<Product>;

const product: ImmutableProduct = getProduct();
// product.price = 100; // ❌ Error: Cannot assign to 'price'

// Record: Objeto con claves tipadas
type ProductsByCategory = Record<string, Product[]>;

const products: ProductsByCategory = {
  electronics: [laptop, phone],
  books: [book1, book2],
};

// Extract: Extraer tipos de una unión
type Status = 'pending' | 'approved' | 'rejected' | 'cancelled';
type ActiveStatus = Extract<Status, 'pending' | 'approved'>; // 'pending' | 'approved'

// Exclude: Excluir tipos de una unión
type InactiveStatus = Exclude<Status, 'pending' | 'approved'>; // 'rejected' | 'cancelled'

// ReturnType: Tipo de retorno de una función
function getUser() {
  return { id: '1', name: 'Lewis', role: 'admin' };
}

type User = ReturnType<typeof getUser>; // { id: string; name: string; role: string }

3. Type Guards: Validación Inteligente

// Type predicates
function isString(value: unknown): value is string {
  return typeof value === 'string';
}

function processValue(value: string | number) {
  if (isString(value)) {
    // TypeScript SABE que aquí es string
    console.log(value.toUpperCase());
  } else {
    // Y aquí es number
    console.log(value.toFixed(2));
  }
}

// Discriminated Unions
type Success = {
  status: 'success';
  data: User;
};

type Error = {
  status: 'error';
  message: string;
  code: number;
};

type Result = Success | Error;

function handleResult(result: Result) {
  if (result.status === 'success') {
    // TypeScript sabe que result.data existe
    console.log(result.data.name);
  } else {
    // Y aquí sabe que result.message existe
    console.log(result.message, result.code);
  }
}

// Custom type guards avanzados
interface Dog {
  breed: string;
  bark: () => void;
}

interface Cat {
  color: string;
  meow: () => void;
}

function isDog(animal: Dog | Cat): animal is Dog {
  return 'bark' in animal;
}

function makeSound(animal: Dog | Cat) {
  if (isDog(animal)) {
    animal.bark(); // ✅ TypeScript sabe que es Dog
  } else {
    animal.meow(); // ✅ Y aquí es Cat
  }
}

4. Mapped Types: Transformaciones Poderosas

// Hacer todos los campos nullable
type Nullable<T> = {
  [P in keyof T]: T[P] | null;
};

type NullableUser = Nullable<User>;
// { name: string | null; age: number | null }

// Hacer campos opcionales basados en condición
type OptionalExcept<T, K extends keyof T> = Partial<T> & Pick<T, K>;

type UpdateUser = OptionalExcept<User, 'id'>;
// { id: string; name?: string; age?: number }

// Agregar prefijo a todas las keys
type Prefixed<T, P extends string> = {
  [K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
};

type PrefixedUser = Prefixed<User, 'user'>;
// { userName: string; userAge: number }

// Caso real: API responses
interface ApiResponse<T> {
  data: T;
  meta: {
    timestamp: Date;
    requestId: string;
  };
}

type UserResponse = ApiResponse<User>;
type ProductsResponse = ApiResponse<Product[]>;

5. Decoradores: Metaprogramación Elegante

// Decorador de logging
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  
  descriptor.value = async function (...args: any[]) {
    console.log(`[${propertyKey}] Llamado con:`, args);
    const start = Date.now();
    
    try {
      const result = await originalMethod.apply(this, args);
      console.log(`[${propertyKey}] Completado en ${Date.now() - start}ms`);
      return result;
    } catch (error) {
      console.error(`[${propertyKey}] Error:`, error);
      throw error;
    }
  };
  
  return descriptor;
}

// Decorador de validación
function Validate(schema: any) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    
    descriptor.value = async function (...args: any[]) {
      const [dto] = args;
      const errors = validateSchema(schema, dto);
      
      if (errors.length > 0) {
        throw new ValidationException(errors);
      }
      
      return originalMethod.apply(this, args);
    };
    
    return descriptor;
  };
}

// Decorador de caché
function Cache(ttl: number = 60000) {
  const cache = new Map<string, { value: any; expiry: number }>();
  
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    
    descriptor.value = async function (...args: any[]) {
      const key = `${propertyKey}:${JSON.stringify(args)}`;
      const cached = cache.get(key);
      
      if (cached && cached.expiry > Date.now()) {
        return cached.value;
      }
      
      const result = await originalMethod.apply(this, args);
      cache.set(key, { value: result, expiry: Date.now() + ttl });
      
      return result;
    };
    
    return descriptor;
  };
}

// Uso en NestJS
@Injectable()
export class UserService {
  @Log
  @Cache(30000)
  async getUser(id: string): Promise<User> {
    return this.userRepository.findById(id);
  }
  
  @Log
  @Validate(CreateUserSchema)
  async createUser(dto: CreateUserDto): Promise<User> {
    return this.userRepository.save(dto);
  }
}

6. Conditional Types: Lógica a Nivel de Tipos

// Tipo condicional básico
type IsString<T> = T extends string ? true : false;

type A = IsString<string>;  // true
type B = IsString<number>;  // false

// Unwrap Promise
type Awaited<T> = T extends Promise<infer U> ? U : T;

type Result = Awaited<Promise<User>>;  // User
type Result2 = Awaited<string>;        // string

// Flatten array
type Flatten<T> = T extends Array<infer U> ? U : T;

type Numbers = Flatten<number[]>;  // number
type Single = Flatten<string>;     // string

// Caso real: Endpoint return types
type ApiEndpoint = {
  '/users': User[];
  '/users/:id': User;
  '/products': Product[];
  '/products/:id': Product;
};

type EndpointResponse<T extends keyof ApiEndpoint> = ApiEndpoint[T];

const users: EndpointResponse<'/users'> = []; // User[]
const user: EndpointResponse<'/users/:id'> = {} as User; // User

7. Template Literal Types: Strings Tipados

// Rutas de API tipadas
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Resource = 'users' | 'products' | 'orders';

type ApiRoute = `/${Resource}`;
// "/users" | "/products" | "/orders"

type ApiRouteWithId = `/${Resource}/${string}`;
// "/users/123" | "/products/abc" | ...

type ApiEndpoint = `${HttpMethod} ${ApiRoute}`;
// "GET /users" | "POST /users" | "PUT /products" | ...

// Eventos tipados
type EventName = 'click' | 'focus' | 'change';
type ElementId = 'button' | 'input' | 'select';

type DomEvent = `${ElementId}:${EventName}`;
// "button:click" | "input:focus" | "select:change"

// CSS properties
type CSSProperty = 'color' | 'fontSize' | 'margin';
type CSSValue = string | number;

type CSSRule = `${CSSProperty}: ${CSSValue}`;

// Caso real: Database queries
type TableName = 'users' | 'products' | 'orders';
type QueryType = 'SELECT' | 'INSERT' | 'UPDATE' | 'DELETE';

type Query = `${QueryType} FROM ${TableName}`;
// "SELECT FROM users" | "INSERT FROM products" | ...

8. Infer: Extracción de Tipos Avanzada

// Extraer tipo de retorno
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function getUser(): User { return {} as User; }
type UserType = GetReturnType<typeof getUser>; // User

// Extraer tipo de parámetros
type GetFirstParam<T> = T extends (first: infer F, ...args: any[]) => any ? F : never;

function createUser(dto: CreateUserDto, options: Options): User {
  return {} as User;
}

type CreateUserDtoType = GetFirstParam<typeof createUser>; // CreateUserDto

// Extraer tipo de array
type ArrayElement<T> = T extends (infer U)[] ? U : never;

type Numbers = number[];
type NumberType = ArrayElement<Numbers>; // number

// Extraer tipo de Promise
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

async function fetchUser(): Promise<User> {
  return {} as User;
}

type FetchUserReturn = UnwrapPromise<ReturnType<typeof fetchUser>>; // User

9. Branded Types: Type Safety Extremo

// Evitar confusión entre tipos primitivos similares
type UserId = string & { readonly __brand: 'UserId' };
type ProductId = string & { readonly __brand: 'ProductId' };
type Email = string & { readonly __brand: 'Email' };

function createUserId(id: string): UserId {
  return id as UserId;
}

function createProductId(id: string): ProductId {
  return id as ProductId;
}

function createEmail(email: string): Email {
  if (!email.includes('@')) {
    throw new Error('Invalid email');
  }
  return email as Email;
}

// Uso
function getUser(id: UserId): User {
  // ...
}

function getProduct(id: ProductId): Product {
  // ...
}

const userId = createUserId('user-123');
const productId = createProductId('prod-456');

getUser(userId);      // ✅ OK
// getUser(productId); // ❌ Error: ProductId no es UserId
// getUser('user-123'); // ❌ Error: string no es UserId

// Caso real: Monedas
type USD = number & { readonly __currency: 'USD' };
type EUR = number & { readonly __currency: 'EUR' };

function toUSD(amount: number): USD {
  return amount as USD;
}

function toEUR(amount: number): EUR {
  return amount as EUR;
}

function processPayment(amount: USD) {
  // Garantizado que es USD
}

const usd = toUSD(100);
const eur = toEUR(100);

processPayment(usd);  // ✅ OK
// processPayment(eur); // ❌ Error: EUR no es USD

10. Const Assertions: Inmutabilidad Profunda

// Sin const assertion
const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3,
};
// Type: { apiUrl: string; timeout: number; retries: number }

// Con const assertion
const CONFIG = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3,
} as const;
// Type: { readonly apiUrl: "https://api.example.com"; readonly timeout: 5000; readonly retries: 3 }

// Arrays readonly
const STATUSES = ['pending', 'approved', 'rejected'] as const;
// Type: readonly ["pending", "approved", "rejected"]

type Status = typeof STATUSES[number]; // "pending" | "approved" | "rejected"

// Caso real: Configuración de rutas
const ROUTES = {
  home: '/',
  about: '/about',
  products: '/products',
  contact: '/contact',
} as const;

type RouteName = keyof typeof ROUTES; // "home" | "about" | "products" | "contact"
type RoutePath = typeof ROUTES[RouteName]; // "/" | "/about" | "/products" | "/contact"

function navigate(route: RoutePath) {
  // ...
}

navigate(ROUTES.home); // ✅ OK
navigate('/products'); // ✅ OK
// navigate('/invalid'); // ❌ Error

Caso Real: MATIAS IMPULSO (SaaS de IA)

Arquitectura TypeScript completa:

// Domain Layer
export class Conversation {
  private constructor(
    public readonly id: ConversationId,
    private messages: Message[],
    private status: ConversationStatus,
    private metadata: ConversationMetadata
  ) {}
  
  static create(userId: UserId): Conversation {
    return new Conversation(
      ConversationId.generate(),
      [],
      'active',
      { userId, createdAt: new Date() }
    );
  }
  
  addMessage(content: string, role: 'user' | 'assistant'): void {
    if (this.status !== 'active') {
      throw new InactiveConversationError(this.id);
    }
    
    this.messages.push(
      Message.create(content, role, this.id)
    );
  }
  
  getContext(): string {
    return this.messages
      .slice(-10) // Últimos 10 mensajes
      .map(m => `${m.role}: ${m.content}`)
      .join('\n');
  }
}

// Application Layer
export class SendMessageUseCase {
  constructor(
    private conversationRepo: ConversationRepository,
    private aiService: AIService,
    private eventBus: EventBus
  ) {}
  
  async execute(request: SendMessageRequest): Promise<SendMessageResponse> {
    const conversation = await this.conversationRepo.findById(request.conversationId);
    
    conversation.addMessage(request.content, 'user');
    
    const context = conversation.getContext();
    const aiResponse = await this.aiService.generateResponse(context);
    
    conversation.addMessage(aiResponse.text, 'assistant');
    
    await this.conversationRepo.save(conversation);
    await this.eventBus.publish(new MessageSentEvent(conversation.id));
    
    return {
      conversationId: conversation.id.value,
      response: aiResponse.text,
      tokensUsed: aiResponse.tokensUsed,
    };
  }
}

// Infrastructure Layer
export class LambdaAIService implements AIService {
  async generateResponse(context: string): Promise<AIResponse> {
    const response = await this.lambda.invoke({
      FunctionName: 'ai-chat-handler',
      Payload: JSON.stringify({ context }),
    });
    
    return AIResponseMapper.fromLambda(response);
  }
}

Resultados:

  • Zero runtime errors por tipos incorrectos
  • 95% de cobertura de tipos
  • Refactoring seguro con IDE autocompletion
  • Documentación automática con tipos

Configuración TypeScript Profesional

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",
    
    // Type Checking estricto
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    
    // Checks adicionales
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    
    // Module Resolution
    "moduleResolution": "node",
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "allowSyntheticDefaultImports": true,
    
    // Advanced
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    
    // Source Maps
    "sourceMap": true,
    "declaration": true,
    "declarationMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.spec.ts"]
}

Herramientas Esenciales

{
  "devDependencies": {
    "typescript": "^5.3.0",
    "@typescript-eslint/parser": "^6.0.0",
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "ts-node": "^10.9.0",
    "tsx": "^4.7.0",
    "tsc-alias": "^1.8.0",
    "@types/node": "^20.10.0"
  },
  "scripts": {
    "build": "tsc && tsc-alias",
    "dev": "tsx watch src/index.ts",
    "type-check": "tsc --noEmit",
    "lint": "eslint . --ext .ts"
  }
}

Checklist de TypeScript Profesional

  • ¿Strict mode activado?
  • ¿Cero any en el código?
  • ¿Tipos exportados para librerías?
  • ¿Generics donde sea apropiado?
  • ¿Type guards para uniones complejas?
  • ¿Utility types en vez de tipos manuales?
  • ¿Branded types para IDs y valores críticos?
  • ¿Const assertions para configuración?
  • ¿Tests de tipos con @ts-expect-error?

Conclusión

TypeScript no es opcional en 2025. Es seguro de vida para tu código.

Los mejores desarrolladores TypeScript:

  • 🎯 Usan el sistema de tipos como documentación
  • 🛡️ Aprovechan strict mode al máximo
  • 🔧 Crean tipos reutilizables con generics
  • 📚 Prefieren inferencia sobre anotaciones explícitas

Invierte tiempo en aprender TypeScript. Te ahorrará semanas de debugging.


Lewis Lopez - TypeScript en producción desde 2019
APIs bancarias 100% type-safe
Fundador de MATIAS IMPULSO

¿Dudas de TypeScript? contacto@lewislopez.io

Compartir este artículo:

¿Te gustó este artículo?

Aprende arquitectura de élite y transforma tu carrera de programador 1x a arquitecto 10x.