Tienes razón en estar confundido. Las firmas de índice significan algunas cosas, y significan cosas ligeramente diferentes dependiendo de dónde y cómo pregunte.
Primero, las firmas de índice implican que todas las propiedades declaradas en el tipo deben tener un tipo compatible .
interface NotLegal {
// Error, 'string' isn't assignable to 'number'
x: string;
[key: string]: number;
}
Este es un aspecto definitorio de las firmas de índice: que describen un objeto con diferentes claves de propiedad, pero un tipo consistente en todas esas claves. Esta regla evita que se observe un tipo incorrecto cuando se accede a una propiedad a través de una indirección:
function fn(obj: NotLegal) {
// 'n' would have a 'string' value
const n: number = obj[String.fromCharCode(120)];
}
En segundo lugar, las firmas de índice permiten escribir en cualquier índice con un tipo compatible .
interface NameMap {
[name: string]: number;
}
function setAge(ageLookup: NameMap, name: string, age: number) {
ageLookup[name] = age;
}
Este es un caso de uso de clave para firmas de índice: tiene un conjunto de claves y desea almacenar un valor asociado con la clave.
Tercero, las firmas de índice implican la existencia de cualquier propiedad que solicite específicamente :
interface NameMap {
[name: string]: number;
}
function getMyAge(ageLookup: NameMap) {
// Inferred return type is 'number'
return ageLookup["RyanC"];
}
Debido a que x["p"]
y x.p
tienen un comportamiento idéntico en JavaScript, TypeScript los trata de manera equivalente:
// Equivalent
function getMyAge(ageLookup: NameMap) {
return ageLookup.RyanC;
}
Esto es coherente con la forma en que TypeScript ve las matrices, que se supone que el acceso a la matriz está dentro de los límites. También es ergonómico para las firmas de índice porque, muy comúnmente, tiene un conjunto conocido de claves disponibles y no necesita realizar ninguna verificación adicional:
interface NameMap {
[name: string]: number;
}
function getAges(ageLookup: NameMap) {
const ages = [];
for (const k of Object.keys(ageLookup)) {
ages.push(ageLookup[k]);
}
return ages;
}
Sin embargo, las firmas de índice no implican que haya una propiedad arbitraria e inespecífica cuando se trata de relacionar un tipo con una firma de índice con un tipo con propiedades declaradas:
interface Point {
x: number;
y: number;
}
interface NameMap {
[name: string]: number;
}
const m: NameMap = {};
// Not OK, which is good, because p.x is undefined
const p: Point = m;
¡Es muy poco probable que este tipo de tarea sea correcta en la práctica!
Esta es una característica distintiva entre { [k: string]: any }
y en any
sí misma: puede leer y escribir propiedades de cualquier tipo en un objeto con una firma de índice, pero no se puede usar en lugar de ningún tipo como any
puede.
Cada uno de estos comportamientos es individualmente muy justificable, pero en su conjunto, se pueden observar algunas inconsistencias.
Por ejemplo, estas dos funciones son idénticas en términos de su comportamiento en tiempo de ejecución, pero TypeScript solo considera que una de ellas se ha llamado incorrectamente:
interface Point {
x: number;
y: number;
}
interface NameMap {
[name: string]: number;
}
function A(x: NameMap) {
console.log(x.y);
}
function B(x: Point) {
console.log(x.y);
}
const m: NameMap = { };
A(m); // OK
B(m); // Error
En general, cuando escribe una firma de índice en un tipo, dice:
- Está bien leer / escribir este objeto con una clave arbitraria
- Cuando leo una propiedad específica de este objeto a través de una clave arbitraria, siempre está presente
- Este objeto no tiene literalmente todos los nombres de propiedad existentes para fines de compatibilidad de tipos