string não pode ser usada para indexar tipo 'T'

Sep 15 2020

Eu tenho o modo estrito ativado no TypeScript e estou tendo problemas para definir um índice em um objeto, embora tenha configurado um indexador de string em seu tipo.

Desativar noImplicitAny corrige isso, mas eu prefiro não.

Estudei essa resposta com atenção, mas não tive sucesso com as sugestões.

type stringKeyed = { [key: string]: any }
type dog = stringKeyed & { name: string }
type cat = stringKeyed & { lives: number }

function setLayerProps<T extends dog | cat>(
    item: T,
    props: Partial<T>
) {
    if (item) {
        Object.entries(props).forEach(([k, v]) => {
                item[k] = v; // Error: Type 'string' cannot be used to index type 'T'.ts(2536)

        });
    }
}


let d = { name: 'fido' } as dog;
let c = { lives: 9 } as cat;
setLayerProps(d, { name: 'jim' })
setLayerProps(c, { lives: --c.lives })

Alguém pode ver o que estou fazendo de errado?

Obrigado!

Respostas

3 arslan2012 Sep 15 2020 at 10:43

Este é um efeito de # 30769 e uma alteração significativa. Anteriormente, permitíamos coisas malucas como essa sem erros:

function foo<T extends Record<string, any>>(obj: T) {
    obj["s"] = 123;
    obj["n"] = "hello";
}
let z = { n: 1, s: "abc" };
foo(z);
foo([1, 2, 3]);
foo(new Error("wat"));

Em geral, a restrição Record <string, XXX> não garante realmente que um argumento tenha uma assinatura de índice de string, ela apenas garante que as propriedades do argumento podem ser atribuídas ao tipo XXX. Portanto, no exemplo acima, você poderia passar efetivamente qualquer objeto e a função poderia gravar em qualquer propriedade sem nenhuma verificação.

No 3.5, impomos que você só possa gravar em um elemento de assinatura de índice quando sabemos que o objeto em questão tem uma assinatura de índice. Então, você precisa de uma pequena mudança na função clone:

function clone<T extends Record<string, any>>(obj: T): T {
    const objectClone = {} as Record<string, any>;

    for (const prop of Reflect.ownKeys(obj).filter(isString)) {
        objectClone[prop] = obj[prop];
    }

    return objectClone as T;
}

Com essa mudança, agora sabemos que objectClone tem uma assinatura de índice de string.

Portanto, seu código deve ser

function setLayerProps<T extends dog | cat>(
    item: T,
    props: Partial<T>
) {
    if (item) {
        Object.entries(props).forEach(([k, v]) => {
            (item as dog | cat)[k] = v;

        });
    }
}

Referência: https://github.com/microsoft/TypeScript/issues/31661#issuecomment-497138929

2 Al-un Sep 15 2020 at 10:37

Aviso: esta não é realmente uma resposta, mas o resultado de minha perambulação por este tópico. Para maior clareza, estou escrevendo uma resposta.

TL; DR: o tipo de chave do objeto é string | number, nãostring

Fazendo funcionar

Usei o playground do TypeScript e acabei com algo que funciona no TypeScript 4.0.2:

type stringKeyed = { [key: string]: any };
type dog = stringKeyed & { name: string };
type cat = stringKeyed & { lives: number };

function setLayerProps<T extends dog | cat>(item: T, props: Partial<T>) {
  if (item) {
    Object.entries(props).forEach(([k, v]) => {
      // With an explicit key casting, TypeScript does not complain
      const key = k as keyof T;
      item[key] = v;
    });
  }
}

let d = { name: "fido" } as dog;
let c = { lives: 9 } as cat;
setLayerProps(d, { name: "jim" });
setLayerProps(c, { lives: --c.lives });

console.log(d); // [LOG]: { "name": "jim" }
console.log(c); // [LOG]: { "lives": 8 }

Por que isso funciona?

O tipo de interseção é ... não é uma interseção

De acordo com a documentação do TypeScript :

Um tipo de interseção combina vários tipos em um. Isso permite que você adicione tipos existentes para obter um único tipo que tenha todos os recursos de que você precisa.

Droga, eu gostaria de ter prestado mais atenção a essas duas linhas antes.

Eu me enganei com a interseção de palavras e pensei que esse dogtipo permitiria apenas uma tecla ( name), mas não é o caso. De acordo com os tipos de interseção no artigo TypeScript em codingblast , o tipo de interseção faz com que o tipo filho herde todas as propriedades dos tipos com interseção.

Em outras palavras:

type stringKeyed = { [key: string]: any };
type dog = stringKeyed & { name: string };

significa que

  • Para a namechave, um valor de string é esperado
  • Para qualquer outra chave, qualquer valor é permitido

A seguinte declaração está então correta:

let d: dog = {name: 'fido' };
d.something = else;
d.age = 3;

Nesse estágio, a única conclusão que somos tentados a tirar é que todas as dogchaves são apenas strings . Bem ... não exatamente.

JavaScript ... não sabe a diferença entre uma tecla numérica e uma chave de string T_T

Para citar esta excelente resposta StackOverflow :

Conforme definido nas notas de lançamento do TypeScript 2.9, se você digitar uma interface com uma assinatura de índice de string, ela retornará uma união de string e número

E a documentação do TypeScript menciona:

Se o tipo tiver uma string ou assinatura de índice de número, keyof retornará esses tipos:

type Mapish = { [k: string]: boolean };
type M = keyof Mapish;
//   ^ = type M = string | number

Para me convencer, tentei o seguinte:

type keys = keyof stringKeyed;
type dogKeys = keyof dog;
const keyAsStr: dogKeys = "this is OK";
const keyAsNum: dogKeys = 42;

Ambos keyse dogKeysresolvem string | number.

string! == string | número

Agora, a razão pela qual o erro Error: Type 'string' não pode ser usado para indexar o tipo 'T'.ts (2536) é simplesmente porque um tipo de chave nunca pode ser stringúnico. A versão "mínima" é string | number, daí o erro de digitação. Curiosamente, ao tentar obter o Ttipo de chave no setLayerProps, passar o mouse sobre o tipo não exibe imediatamente o tipo. Então eu tentei com

// Inside the setLayerProps function
type tKey = keyof T;
const asStr: tKey = "43";
const asNum: tKey = 43;
const asObj: tKey = { a: "a" };

E acabei com o seguinte erro:

Type '{ a: string; }' is not assignable to type 'keyof T'.
  Type '{ a: string; }' is not assignable to type 'string | number'.
    Type '{ a: string; }' is not assignable to type 'number'.

Portanto, o TypeScript estava realmente esperando umstring | number .

Por algum motivo que ainda não entendo, isso não funciona:

const key = k as string | number;

Como isso leva a erros:

Type 'number' cannot be used to index type 'T'.(2536)
Type 'string' cannot be used to index type 'T'.(2536)

Por que o elenco explícito é seguro

Na setLayerPropsfunção, sabemos que a itemchave é a stringou a number. Casting tão explicitamente

const key = k as keyof T;

significa que estou dizendo que uma string é uma string ou um número , o que, em minha opinião, é sempre verdadeiro.