O que realmente significa uma assinatura de índice TypeScript?

8

Estou escrevendo o TypeScript há um tempo e estou confuso sobre o que significa uma assinatura de índice.

Por exemplo, este código é legal:

function fn(obj: { [x: string]: number }) {
    let n: number = obj.something;
}

Mas esse código, que basicamente faz a mesma coisa, não é:

function fn(obj: { [x: string]: number }) {
    let p: { something: number } = obj;
}

Isso é um inseto? Qual é o significado pretendido disso?

Ryan Cavanaugh
fonte

Respostas:

9

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
Ryan Cavanaugh
fonte