Pesquisa de site

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);

Artigos relacionados