TypeScript e parâmetro de tipo não genérico
Considere a seguinte definição de tipo mapeado:
type FeaturesMapEx<T> = {
[K in keyof T]?: T[K];
};
Qual é um tipo mapeado genérico válido, que usa o tipo genérico T e os campos (chaves) de T como um tipo de índice próprio. Esses campos armazenam valores do respectivo tipo T[K], o que basicamente se traduz em
"Aqui você tem um tipo genérico FeaturesMapEx de T. Use T para obter seus campos e usá-los como seus próprios. Se T[K] for do tipo X, então FeaturesMapEx<T>[K] é do mesmo tipo"
Ok. Agora a coisa que eu realmente não entendo, por que a declaração não precisa ser assim, usando dois parâmetros genéricos, um para representar o Tipo, o segundo para representar as Chaves desse Tipo:
export type FeaturesMapEx<T, K extends keyof T> = {
[K in keyof T]?: T[K];
};
A declaração acima é válida, mas K não é usado, [K in keyof T] usa um K diferente do que foi definido na declaração genérica.
Ou ainda melhor (mas não válido) declaração que faz mais sentido comum para mim:
export type FeaturesMapEx<T, K extends keyof T> = {
[K]?: T[K];
};
O que gera o erro do TypeScript da seguinte forma:
Um nome de propriedade computada em um literal de tipo deve se referir a uma expressão cujo tipo seja um tipo literal ou um tipo 'símbolo único'.ts(1170)
'K' só se refere a um tipo, mas está sendo usado como um valor aqui.ts(2693)
Embora me sinta confortável com o uso de tipos mapeados e genéricos, este exemplo sempre me deixa um pouco tonto quando tenho que escrevê-lo sozinho após algum tempo, eu sempre acabo caindo nos padrões apresentados depois (dois genéricos ou erro de "tipo usado como valor")... alguém pode explicar se minha confusão é comum ou se estou perdendo alguma coisa?
Uma pergunta extra é: de onde vem o tipo K na primeira declaração, se ele não for fornecido pelo parâmetro de tipo genérico?
Resposta #1
Considere a interface
interface Foo {
a: string,
b: number
}
e o tipo mapeado
export type FeaturesMapEx<T> = {
[K in keyof T]?: T[K];
};
Em um tipo mapeado assim, estamos introduzindo um novo parâmetro de tipo K (é de onde ele vem) que está iterando sobre keyof T. Supondo que keyof T seja um tipo união, K itera sucessivamente sobre cada constituinte dessa união. Você pode meio que pensar nisso como a versão em nível de tipo do seguinte for em tempo de execução:
function featuresMapEx(t: any) {
for (let k of Object.keys(t)) {
t[k];
}
}
Observe como t é introduzido na assinatura da função (como o T na definição do alias de tipo), enquanto k é introduzido em um loop e só existe no escopo desse loop (como o K é introduzido na chave do tipo mapeado, e só existe no escopo da propriedade do tipo mapeado).
Então FeaturesMapEx<Foo> se torna {a: string, b: number}.
Se eu escrevesse a seguinte função JavaScript em vez disso:
function featuresMapEx(t: any, k: any) { // unused parameter here
for (let k of Object.keys(t)) { // oops
t[k];
}
}
você pode ver que está ignorando completamente o k passado para a função, muito parecido com
export type FeaturesMapEx<T, K extends keyof T> = { // unused parameter here
[K in keyof T]?: T[K]; // oops
};
ignora o K passado para FeaturesMapEx<T, K>.
Então este FeaturesMapEx<Foo, 'a'> ainda se torna {a: string, b: number}.
Quanto à sua outra sintaxe:
export type FeaturesMapEx<T, K extends keyof T> = {
[K]?: T[K]; // error
};
Isso não seria mais um tipo mapeado de forma alguma, mas uma assinatura de índice, e atualmente você não pode usar tipos de chave arbitrários em uma assinatura de índice (embora isso possa acontecer após o TS3.5 em algum momento). Um tipo mapeado se parece com uma assinatura de índice, mas não é uma. Mesmo que isso acabe fazendo parte da linguagem, não seria realmente a mesma coisa... provavelmente funcionaria assim:
function featuresMapEx(t: any, k: any) {
t[k]; // just one thing
}
Ou seja, ele realmente não iria iterar sobre nada. Você pode meio que obter esse comportamento agora assim:
export type FeaturesMapEx<T, K extends keyof T> = Partial<Record<T, K>>
que é o mesmo que o tipo mapeado
export type FeaturesMapEx<T, K extends keyof T> = {
[P in K]?: T[K]
}
mas este FeaturesMapEx<Foo, keyof Foo> se torna {a: string | number, b: string | number}. Ou seja, as chaves estariam corretas, mas os valores seriam a união dos valores que você espera.
De qualquer forma, espero que a comparação com funções de tempo de execução ajude a fazer você 'entender'. Boa sorte!
Resposta #2
...de onde vem o tipo K na primeira declaração, se ele não é fornecido pelo parâmetro de tipo genérico?
Na sua primeira declaração, o tipo K é introduzido no loop.
type FeaturesMapEx<T> = {
[K in keyof T]?: T[K]; // this is the loop
};
Aqui está uma analogia solta que usa uma função JavaScript:
const someObj = {
foo: 'foo-value',
bar: 'bar-value',
baz: 'baz-value',
}
function featuresMapEx(T) {
return Object.keys(T).map(K => `${K}: ${T[K]}`);
}
const result = featuresMapEx(someObj);
console.log(result);