Você está certo em ficar confuso. As assinaturas de índice significam algumas coisas, e significam coisas ligeiramente diferentes, dependendo de onde e como você pergunta.
Primeiro, as assinaturas de índice implicam que todas as propriedades declaradas no tipo devem ter um tipo compatível .
interface NotLegal {
// Error, 'string' isn't assignable to 'number'
x: string;
[key: string]: number;
}
Esse é um aspecto de definição das assinaturas de índice - que descrevem um objeto com diferentes chaves de propriedade, mas um tipo consistente em todas essas chaves. Esta regra evita que um tipo incorreto seja observado quando uma propriedade é acessada por meio de um indireto:
function fn(obj: NotLegal) {
// 'n' would have a 'string' value
const n: number = obj[String.fromCharCode(120)];
}
Segundo, as assinaturas de índice permitem gravar em qualquer índice com um tipo compatível .
interface NameMap {
[name: string]: number;
}
function setAge(ageLookup: NameMap, name: string, age: number) {
ageLookup[name] = age;
}
Este é um caso de uso chave para assinaturas de índice: você tem algum conjunto de chaves e deseja armazenar um valor associado à chave.
Terceiro, as assinaturas de índice implicam a existência de qualquer propriedade que você solicita especificamente :
interface NameMap {
[name: string]: number;
}
function getMyAge(ageLookup: NameMap) {
// Inferred return type is 'number'
return ageLookup["RyanC"];
}
Porque x["p"]e x.ptêm um comportamento idêntico em JavaScript, texto datilografado trata-los equivalentemente:
// Equivalent
function getMyAge(ageLookup: NameMap) {
return ageLookup.RyanC;
}
Isso é consistente com a maneira como o TypeScript visualiza as matrizes, que é o acesso à matriz dentro dos limites. Também é ergonômico para assinaturas de índice porque, geralmente, você tem um conjunto conhecido de chaves disponíveis e não precisa fazer nenhuma verificação 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;
}
No entanto, as assinaturas de índice não significam que nenhuma propriedade arbitrária e inespecífica esteja presente quando se trata de relacionar um tipo com uma assinatura de índice a um tipo com propriedades 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;
É improvável que esse tipo de tarefa seja correta na prática!
Esta é uma característica que distingue entre { [k: string]: any }e anysi - você pode ler e propriedades de gravação de qualquer tipo em um objeto com uma assinatura de índice, mas não pode ser usado no lugar de qualquer tipo, como anylata.
Cada um desses comportamentos é individualmente muito justificável, mas, no conjunto, algumas inconsistências são observáveis.
Por exemplo, essas duas funções são idênticas em termos de comportamento em tempo de execução, mas o TypeScript considera apenas que uma delas foi chamada incorretamente:
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
No geral, quando você escreve uma assinatura de índice em um tipo, está dizendo:
- Não há problema em ler / gravar este objeto com uma chave arbitrária
- Quando leio uma propriedade específica desse objeto por meio de chave arbitrária, ela está sempre presente
- Esse objeto não possui literalmente todos os nomes de propriedades para fins de compatibilidade de tipos