Pesquisa de site

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

Artigos relacionados