Como definir um tipo TypeScript como um dicionário de strings, mas com uma propriedade numérica "id"
Meu código JavaScript existente tem "registros" onde o id é numérico e os outros atributos são strings. Estou tentando definir este tipo:
type T = {
id: number;
[key: string]: string
}
mas eu recebo o erro 2411: Propriedade 'id' do tipo 'number' não pode ser atribuída ao tipo de índice 'string'.
Resposta #1
Não existe um tipo específico em TypeScript que corresponda à estrutura desejada. Assinaturas de índice de string devem se aplicar a toda propriedade, inclusive às declaradas manualmente, como id. O que você está procurando é algo como uma "assinatura de índice rest" ou um "tipo padrão de propriedade", e existe uma sugestão aberta no GitHub solicitando isso: microsoft/TypeScript#17867. Há algum tempo, houve algum trabalho que teria permitido isso, mas foi abandonado (veja este comentário para mais informações). Então, não está claro quando ou se isso ocorrerá.
Você poderia ampliar o tipo da propriedade de assinatura de índice para que inclua as propriedades codificadas diretamente via uma união, como
type WidenedT = {
id: number;
[key: string]: string | number
}
mas então você teria que testar cada propriedade dinâmica antes de poder tratá-la como uma string:
function processWidenedT(t: WidenedT) {
t.id.toFixed(); // okay
t.random.toUpperCase(); // error
if (typeof t.random === "string") t.random.toUpperCase(); // okay
}
A melhor forma de proceder aqui seria se você pudesse refatorar seu JavaScript para que ele não "misture" o saco de propriedades com valor string com um id de valor number. Por exemplo:
type RefactoredT = {
id: number;
props: { [k: string]: string };
}
Aqui id e props são completamente separados e você não precisa fazer nenhuma lógica de tipo complicada para descobrir se suas propriedades têm valor number ou string. Mas isso exigiria várias mudanças no seu JavaScript existente e pode não ser viável.
Daqui em diante vou assumir que você não pode refatorar seu JavaScript. Mas perceba como o acima está limpo em comparação com a bagunça que está por vir:
Uma solução comum para a ausência de assinaturas de índice de descanso é usar um tipo de interseção para contornar a restrição de que as assinaturas de índice devem se aplicar a todas as propriedades:
type IntersectionT = {
id: number;
} & { [k: string]: string };
Funciona mais ou menos; quando é fornecido um valor do tipo IntersectionT, o compilador vê a propriedade id como um number e qualquer outra propriedade como um string:
function processT(t: IntersectionT) {
t.id.toFixed(); // okay
t.random.toUpperCase(); // okay
t.id = 1; // okay
t.random = "hello"; // okay
}
Mas não é realmente seguro quanto ao tipo, já que você está tecnicamente afirmando que id é tanto um number (de acordo com o primeiro membro da interseção) quanto uma string (de acordo com o segundo membro da interseção). E, infelizmente, você não pode atribuir um literal de objeto a esse tipo sem que o compilador reclame:
t = { id: 1, random: "hello" }; // error!
// Property 'id' is incompatible with index signature.
Você precisa contornar isso ainda mais fazendo algo como Object.assign():
const propBag: { [k: string]: string } = { random: "" };
t = Object.assign({ id: 1 }, propBag);
Mas isso é irritante, já que a maioria dos usuários nunca vai pensar em sintetizar um objeto de maneira tão indireta.
Uma abordagem diferente é usar um tipo genérico para representar seu tipo em vez de um tipo específico. Pense em escrever um verificador de tipo que recebe como entrada um tipo candidato e retorna algo compatível se e somente se esse tipo candidato corresponder à estrutura desejada:
type VerifyT<T> = { id: number } & { [K in keyof T]: K extends "id" ? unknown : string };
Isso exigirá uma função auxiliar genérica para que você possa inferir o tipo genérico T, assim:
const asT = <T extends VerifyT<T>>(t: T) => t;
Agora o compilador permitirá que você use literais de objeto e os verificará da maneira que você espera:
asT({ id: 1, random: "hello" }); // okay
asT({ id: "hello" }); // error! string is not number
asT({ id: 1, random: 2 }); // error! number is not string
asT({ id: 1, random: "", thing: "", thang: "" }); // okay
É um pouco mais difícil ler um valor desse tipo com chaves desconhecidas, porém. A propriedade id está ok, mas outras propriedades não serão conhecidas como existentes, e você receberá um erro:
function processT2<T extends VerifyT<T>>(t: T) {
t.id.toFixed(); // okay
t.random.toUpperCase(); // error! random not known to be a property
}
Finalmente, você pode usar uma abordagem híbrida que combina os melhores aspectos dos tipos de interseção e genéricos. Use o tipo genérico para criar valores e o tipo de interseção para lê-los:
function processT3<T extends VerifyT<T>>(t: T): void;
function processT3(t: IntersectionT): void {
t.id.toFixed();
if ("random" in t)
t.random.toUpperCase(); // okay
}
processT3({ id: 1, random: "hello" });
O acima é uma função sobrecarregada, onde chamadores veem o tipo genérico, mas a implementação vê o tipo de interseção.
Link do playground para o código
