genéricos - Em TypeScript, como obter as chaves de um tipo de objeto cujos valores são de um determinado tipo?
Tenho tentado criar um tipo que consiste nas chaves do tipo T cujos valores são strings. Em pseudocódigo, seria keyof T onde T[P] é uma string.
A única maneira que consigo pensar em fazer isso é em dois passos:
// a mapped type that filters out properties that aren't strings via a conditional type
type StringValueKeys<T> = { [P in keyof T]: T[P] extends string ? T[P] : never };
// all keys of the above type
type Key<T> = keyof StringValueKeys<T>;
No entanto, o compilador TS está dizendo que Key<T> é simplesmente igual a keyof T, mesmo que eu tenha filtrado as chaves cujos valores não são strings, definindo-as como never usando um tipo condicional.
Então ainda está permitindo isso, por exemplo:
interface Thing {
id: string;
price: number;
other: { stuff: boolean };
}
const key: Key<Thing> = 'other';
quando o único valor permitido de key realmente deveria ser "id", e não "id" | "price" | "other", já que os valores das outras duas chaves não são strings.
Link para um exemplo de código no playground do TypeScript
Resposta #1
Há uma solicitação de recurso em microsoft/TypeScript#48992 para suportar isso de forma nativa. Até que isso seja implementado, você pode criar sua própria versão de várias maneiras.
Uma maneira é com tipos condicionais e tipos de acesso indexado, assim:
type KeysMatching<T, V> = {[K in keyof T]-?: T[K] extends V ? K : never}[keyof T];
e então você retira as chaves cujas propriedades correspondem a string assim:
const key: KeysMatching<Thing, string> = 'other'; // ERROR!
// '"other"' is not assignable to type '"id"'
Em detalhe:
KeysMatching<Thing, string> ➡
{[K in keyof Thing]-?: Thing[K] extends string ? K : never}[keyof Thing] ➡
{
id: string extends string ? 'id' : never;
price: number extends string ? 'number' : never;
other: { stuff: boolean } extends string ? 'other' : never;
}['id'|'price'|'other'] ➡
{ id: 'id', price: never, other: never }['id' | 'price' | 'other'] ➡
'id' | never | never ➡
'id'
Observe que o que você estava fazendo:
type SetNonStringToNever<T> = { [P in keyof T]: T[P] extends string ? T[P] : never };
estava realmente apenas transformando os valores das propriedades que não são strings em valores de propriedade never. Não estava mexendo nas chaves. Seu Thing se tornaria {id: string, price: never, other: never}. E as chaves disso são as mesmas das chaves de Thing. A principal diferença entre isso e KeysMatching é que você deve estar selecionando chaves, não valores (então P e não T[P]).
Link do playground para o código
Resposta #2
Como uma resposta adicional:
Desde a versão 4.1, você pode aproveitar o remapeamento de chaves para uma solução alternativa (observe que a lógica central não difere da resposta de jcalz). Basta filtrar as chaves que, quando usadas para indexar o tipo de origem, não produzem um tipo atribuível ao tipo de destino e extrair a união das chaves restantes com keyof:
type KeysWithValsOfType<T,V> = keyof { [ P in keyof T as T[P] extends V ? P : never ] : P };
interface Thing {
id: string;
price: number;
test: number;
other: { stuff: boolean };
}
type keys1 = KeysWithValsOfType<Thing, string>; //id -> ok
type keys2 = KeysWithValsOfType<Thing, number>; //price|test -> ok
Parque infantil
Como mencionado corretamente por Michal Minich:
Ambos podem extrair a união das chaves de string. No entanto, quando devem ser usados em situações mais complexas - como T extends Keys...
- o TS não é capaz de "entender" bem a sua solução.
Porque o tipo acima não indexa com keyof T e, em vez disso, usa keyof do tipo mapeado, o compilador não pode inferir que T é indexável pela união de saída. Para garantir isso ao compilador, pode-se intersectar o último com keyof T:
type KeysWithValsOfType<T,V> = keyof { [ P in keyof T as T[P] extends V ? P : never ] : P } & keyof T;
function getNumValueC<T, K extends KeysWithValsOfType<T, number>>(thing: T, key: K) {
return thing[key]; //OK
}
Playground Atualizado
