Index signatures, Record y claves dinámicas en TypeScript | Nicolás Garzón
Una index signature describe objetos con claves no conocidas individualmente, pero cuyos valores siguen un contrato común.
TypeScript
Copiar type Inventory = {
[ productId: string ] : number ;
} ;
TypeScript
Copiar type Scores = {
[ playerId: string ] : number ;
} ; TypeScript
Copiar const scores: Scores = {
playerA: 10 ,
playerB: 20 ,
} ; TypeScript
Copiar type Scores = {
total: number ;
[ playerId: string ] : number ;
} ; total también debe ser compatible con number.
TypeScript
Copiar type Inventory = {
total: number ;
quantities: Record< string , number > ;
} ; Evita obligar a que todas las propiedades tengan el mismo tipo.
TypeScript
Copiar type Scores = Record< string , number > ; Es un mapped type equivalente en muchos casos a una index signature string.
TypeScript
Copiar type Permission =
| "products.read"
| "products.write"
| "orders.read" ;
type Permissions = Record< Permission, boolean > ; Ahora deben existir todas las claves de la union.
TypeScript
Copiar type OptionalPermissions = Partial<
Record< Permission, boolean >
> ; Las claves pueden faltar.
TypeScript
Copiar const score = scores[ playerId] ; Sin el flag puede verse como number aunque la clave no exista.
Con noUncheckedIndexedAccess:
TypeScript
Copiar number | undefined Refleja mejor el runtime.
TypeScript
Copiar settings. unknownKey; Con este flag, miembros que provienen únicamente de index signature deben accederse con corchetes:
TypeScript
Copiar settings[ "unknownKey" ] ; Hace visible que la propiedad no es un miembro declarado.
TypeScript
Copiar type NumericDictionary = {
[ index: number ] : string ;
} ; Útil para estructuras indexadas numéricamente, aunque arrays y tuples suelen expresar mejor secuencias.
TypeScript
Copiar type SymbolRegistry = {
[ key: symbol ] : Metadata;
} ; TypeScript
Copiar type DataAttributes = {
[ key: ` data- ${ string } ` ] : string | undefined ;
} ; Restringe la forma de claves abiertas.
TypeScript
Copiar type Options = {
width? : number ;
height? : number ;
[ key: ` data- ${ string } ` ] : unknown ;
} ; Permite extensiones con prefijo sin aceptar cualquier typo ordinario.
Record<string, T> representa un objeto y hereda reglas de property keys, prototipo y serialización.
Acepta objetos como claves.
Tiene size.
No mezcla propiedades del prototipo.
Expresa una colección dinámica.
Elige según el runtime, no solo por el tipo.
Para un diccionario sin prototipo:
TypeScript
Copiar const dictionary: Record< string , number > = Object. create ( null ) ; El tipo no comunica por sí solo que el prototipo es null; el valor runtime sí cambia.
TypeScript
Copiar for ( const key of Object. keys ( scores) ) {
const value = scores[ key] ;
} Con noUncheckedIndexedAccess, value incluye undefined. La clave proviene del objeto, pero este tipo general no demuestra presencia a nivel estático.
Una index signature acepta claves adicionales por diseño. No la uses para evitar excess property checking en objetos con un conjunto conocido.
TypeScript
Copiar type Product = {
id: string ;
[ key: string ] : unknown ;
} ; Oculta typos y vuelve cada acceso más amplio.
TypeScript
Copiar type EventMap = {
"product.created" : ProductCreated;
"product.deleted" : ProductDeleted;
} ;
type Handlers = {
[ K in keyof EventMap] ? : (
event: EventMap[ K ] ,
) => void ;
} ; Un mapped type mantiene la relación entre cada clave y su payload.
Usar index signature para un objeto de claves conocidas.
Mezclar metadata incompatible con el valor del diccionario.
Creer que Record<string, T> garantiza que cualquier clave existe.
Ignorar noUncheckedIndexedAccess.
Usar objetos cuando Map expresa mejor la colección.
Añadir [key: string]: any para silenciar errores.
Perder relaciones entre claves y valores usando un solo tipo amplio.
Confiar en Record para proteger contra prototype pollution.
Index signatures modelan claves abiertas.
Las propiedades conocidas deben ser compatibles.
Record puede modelar claves abiertas o unions cerradas.
noUncheckedIndexedAccess refleja ausencia posible.
Pattern signatures restringen prefijos.
Map y object tienen contratos runtime distintos.
No añadas una index signature para esconder typos.
Mapped types conservan relaciones por clave.
¿Por qué Record<string, Product> no garantiza que products["missing"] exista en runtime?
Respuesta Porque el tipo describe el valor esperado para claves presentes, pero un objeto normal puede no tener una clave arbitraria. noUncheckedIndexedAccess hace visible esa ausencia.
Branded y opaque types añade identidad estática a valores estructuralmente iguales.