Mesclar um Record do Typescript ou tipo semelhante a dicionário com tipagens de chave fixa?
Existe uma maneira de estender o Record embutido (ou uma interface { [key:string]: string }) onde você também define algumas chaves fixas e seus tipos?
Digamos que temos isto:
const otherValues = {
some: 'some',
other: 'other',
values: 'values',
}
const composedDictionary = {
id: 1,
...otherValues
}
Quero definir uma interface para composedDictionary onde id seja tipado como number (e apenas número) e todo o resto como string.
Eu tentei isto:
interface ExtendedRecord extends Record<string, string> {
id: number;
}
e isso:
interface MyDictionary {
[key: string]: string;
id: number;
}
Ambos falham com:
Property 'id' of type 'number' is not assignable to string index type 'string'Alguma ideia?
Resposta #1
Idealmente, a assinatura do índice deveria refletir qualquer possível resultado de uma operação de indexação. Se você acessar composedDictionary com uma chave de string não verificável, o resultado pode ser number se essa string for realmente 'id' (por exemplo: composedDictionary['id' as string], a tipagem diria que isto é string mas em tempo de execução acaba sendo number). É por isso que o sistema de tipos está te causando problemas, este é um tipo inconsistente.
Você pode definir seu índice para ser consistente com todas as propriedades:
interface MyDictionary {
[key: string]: string | number;
id: number;
}
Há uma brecha nos tipos de verificação que o TypeScript faz para consistência de índice e propriedade. Essa brecha é tipos de interseção:
type MyDictionary = Record<string, string> & {
id: number
}
const composedDictionary: MyDictionary = Object.assign({
id: 1,
}, {
...otherValues
});
O compilador ainda vai resistir à sua atribuição, e a única maneira de criar um objeto tão inconsistente no sistema de tipos é usando Object.assign
Resposta #2
Como a outra resposta disse, o TypeScript não suporta um tipo no qual algumas propriedades sejam exceções à assinatura de índice... e, portanto, não há como representar seu MyDictionary como um tipo concreto consistente. A solução da interseção inconsistente ({[k: string]: string]} & {id: number}) acontece de funcionar na leitura de propriedades, mas é difícil de usar em escrita de propriedades.
Havia uma sugestão antiga de permitir assinaturas de índice "rest", onde você pode dizer que uma assinatura de índice deve representar todas as propriedades exceto aquelas especificadas.
Há também um par de melhorias mais recentes (mas possivelmente arquivadas) que implementam tipos negados e tipos de chave arbitrária para assinaturas de índice, que permitiriam representar tais propriedades de assinatura de índice de exceção/padrão como algo como { id: number; [k: string & not "id"]: string }. Mas isso ainda não compila (TS3.5) e talvez nunca compile, então isso é apenas um sonho por enquanto.
Então você não pode representar MyDictionary como um tipo concreto. No entanto, você pode representá-lo como uma restrição genérica. Usá-lo de repente exige que todas as suas funções anteriormente concretas se tornem funções genéricas, e que seus valores anteriormente concretos se tornem saídas de funções genéricas. Então, pode ser um mecanismo demais para valer a pena. Ainda assim, vamos ver como fazer isso:
type MyDictionary<T extends object> = { id: number } & {
[K in keyof T]: K extends "id" ? number : string
};
Neste caso, MyDictionary<T> recebe um tipo candidato T e o transforma em uma versão que corresponde ao seu tipo desejado de MyDictionary. Em seguida, usamos a seguinte função auxiliar para verificar se algo corresponde:
const asMyDictionary = <T extends MyDictionary<T>>(dict: T) => dict;
Observe a restrição de autorreferência T extends MyDictionary<T>. Então aqui está o seu caso de uso e como ele funciona:
const otherValues = {
some: "some",
other: "other",
values: "values"
};
const composedDictionary = asMyDictionary({
id: 1,
...otherValues
}); // okay
Isso compila sem erros, porque o parâmetro para asMyDictionary() é um MyDictionary<T> válido. Agora vamos ver algumas falhas:
const invalidDictionary = asMyDictionary({
id: 1,
oops: 2 // error! number is not a string
})
const invalidDictionary2 = asMyDictionary({
some: "some" // error! property id is missing
})
const invalidDictionary3 = asMyDictionary({
id: "oops", // error! string is not a number
some: "some"
})
O compilador detecta cada um desses erros e diz onde está o problema.
Então, isso é o mais próximo que eu consigo chegar do que você quer na versão TS3.5. Ok, espero que ajude; boa sorte!
Link para o código
