Pesquisa de site

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

Artigos relacionados