¿Por qué este tema explota en Stack Overflow?
\n\nSi alguna vez has escrito en TypeScript y has intentado crear un objeto donde todos los valores deben ser del mismo tipo, seguramente te has encontrado con este problema. La pregunta "Enforcing the type of the indexed members of a Typescript object" ha acumulado miles de visitas en Stack Overflow no por casualidad: es un "clásico tropiezo" para los desarrolladores que migran desde JavaScript.
\n\nImagina: tienes un diccionario de caché, una configuración con claves dinámicas o simplemente un mapa con números. Quieres que TypeScript se asegure de que cada valor en el objeto sea una cadena, un número o algún tipo complejo. Pero el compilador, por alguna razón, deja pasar errores. ¿Te suena familiar?
\n\nEn este artículo, analizaremos todos los enfoques principales para resolver esta tarea: desde simples Index Signatures hasta avanzados Mapped Types. Aprenderás cuándo usar cada método y qué trampas te esperan.
El problema: objeto con claves dinámicas
\n\nSupongamos que estamos escribiendo un sistema de gestión de usuarios. Tenemos un objeto donde las claves son los ID de los usuarios y los valores son sus nombres:
\n\ninterface UserCache {\n [userId: string]: string;\n}\n\nconst cache: UserCache = {\n 'user_1': 'Alice',\n 'user_2': 'Bob',\n // 'user_3': 123; // ¡Error! Debe ser una cadena\n};\n\ncache['user_4'] = 'Charlie'; // OK\ncache['user_5'] = 42; // ¡Error! El tipo 'number' no se puede asignar a 'string'\n\n\nParece que funciona. Pero hay un detalle: Index Signature permite cualquier clave de tipo cadena. ¿Y si queremos limitar el conjunto de claves permitidas?
Enfoque 1: Index Signature — el clásico
\n\nIndex Signature es la forma más simple de definir el tipo para todos los valores de un objeto. Sintaxis:
\n\ninterface StringMap {\n [key: string]: string;\n}\n\nconst map: StringMap = {\n hello: 'world',\n foo: 'bar',\n};\n\n\nVentajas:
\n- \n
- Simplicidad y legibilidad \n
- Funciona con cualquier cantidad de claves \n
Desventajas:
\n- \n
- No se puede limitar el conjunto de claves \n
- Todos los valores deben ser del mismo tipo (o unión) \n
- No funciona con valores
undefined(se necesita precaución) \n
Un error común de principiantes: intentar agregar un método a este tipo de objeto:
\n\ninterface BadMap {\n [key: string]: string;\n length: number; // ¡Error! 'number' no se puede asignar a 'string'\n}\n\n\nEsto no funcionará porque length viola el tipo general. Si necesitas mezclar tipos, usa una unión o Mapped Types.
Enfoque 2: Record — un helper incorporado
\n\nTypeScript proporciona el tipo incorporado Record<K, V>, que hace lo mismo pero con una sintaxis más concisa:
type StringRecord = Record<string, string>;\n\nconst config: StringRecord = {\n apiUrl: 'https://api.example.com',\n timeout: '5000', // OK, es una cadena\n // port: 8080, // ¡Error!\n};\n\n// Se pueden limitar las claves:\ntype AllowedKeys = 'name' | 'email' | 'phone';\ntype UserRecord = Record<AllowedKeys, string>;\n\nconst user: UserRecord = {\n name: 'Alice',\n email: 'alice@example.com',\n phone: '+1234567890',\n // age: '30', // ¡Error! 'age' no está en AllowedKeys\n};\n\n\nCuándo usarlo:
\n- \n
- Cuando necesitas crear rápidamente un diccionario tipado \n
- Cuando las claves son un conjunto específico de valores (unión) \n
- Para configuraciones con campos conocidos \n
Desventaja: Record no permite definir diferentes tipos para diferentes claves. Para eso están los Mapped Types.
Enfoque 3: Map — la alternativa de ES6
\n\nSi usas Map de JavaScript, TypeScript lo tipifica excelentemente