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.p
tê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 any
si - 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 any
lata.
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