Estructura de una interface TypeScript
Una interface define la forma (shape) de un objeto: qué propiedades tiene y de qué tipo son. Sintaxis básica: interface User { id: number; name: string; email: string; }. Cada propiedad tiene un nombre, dos puntos, y su tipo. Los tipos primitivos son string, number, boolean, null, undefined.
Propiedades opcionales se marcan con ?: avatar?: string significa que puede existir o no. Propiedades readonly son inmutables: readonly id: number no puede reasignarse después de la creación. Los arrays se tipan con string[] o Array<string>, ambos válidos.
Para objetos anidados, podés definir interfaces inline o referenciar otras interfaces: address: { street: string; city: string } o address: Address donde Address es otra interface. La segunda opción es preferible para reutilización. Los union types combinan opciones: status: 'active' | 'inactive' | 'pending'.
Naming conventions y organización
Los nombres de interfaces usan PascalCase: UserProfile, ApiResponse, ProductDetails. Evitá prefijos redundantes como IUser (herencia de C#, innecesaria en TS). Si tenés un conflicto de nombres, usá sufijos descriptivos: UserEntity vs UserDto vs UserViewModel.
Organizá interfaces por feature o capa. En un proyecto React: components/Button/Button.types.ts para props, api/users/types.ts para responses. No tires todo en un types.ts global; ese archivo crece sin control y nadie sabe qué está usando qué. La excepción: tipos compartidos globalmente van en @types o shared/types.
Para tipos que vienen de APIs externas, considerá autogenerar con herramientas como openapi-typescript o quicktype. Escribir a mano 200 líneas de interface para un response de Stripe es error humano garantizado. Si la API tiene schema OpenAPI/Swagger, generalo; si no, pedí que lo agreguen.
Tipos utilitarios y composición
TypeScript incluye tipos utilitarios que transforman interfaces existentes. Partial<User> hace todas las props opcionales (útil para updates). Required<User> hace lo opuesto. Pick<User, 'id' | 'name'> selecciona solo esas props. Omit<User, 'password'> excluye la prop password.
Record<string, number> es un objeto con keys string y values number, útil para mapeos dinámicos. Readonly<User> hace toda la interface inmutable. NonNullable<T> remueve null/undefined de un tipo. Estos utilitarios evitan duplicación: no necesitás UserUpdate separado si podés usar Partial<User>.
Para composición, extendé interfaces: interface Admin extends User { permissions: string[] }. O usá intersección: type AdminUser = User & { permissions: string[] }. La diferencia: extends es para interfaces (puede redeclararse), & es para types (más flexible pero no redeclarable).
Errores comunes y cómo evitarlos
Error #1: usar any por pereza. data: any destruye el propósito de TypeScript. Si no sabés el tipo exacto, usá unknown y hacé type narrowing con guards. O definí la interface parcialmente e iterá: mejor tener 3 props tipadas que todo en any.
Error #2: interfaces demasiado genéricas. interface Data { value: any } no aporta nada. Sé específico: interface UserData { userId: string; loginCount: number }. Cuanto más concreto el tipo, más errores cachea el compilador.
Error #3: no tipar responses de API. El fetch devuelve any por default. Siempre tipá: const user = await fetch('/api/user').then(r => r.json()) as User o mejor, usá un cliente tipado como tRPC, GraphQL Codegen, o axios con interceptores que typechequeán.
Error #4: duplicar definiciones. Si User en frontend y backend son idénticos, compartí el tipo (monorepo con shared package, o exportá desde backend si es TypeScript también). Mantener dos definiciones sincronizadas manualmente es receta para bugs sutiles donde un campo cambió de tipo y frontend no se enteró.