Búsqueda de sitios web

genéricos - En TypeScript, ¿cómo obtener las claves de un tipo de objeto cuyos valores son de un tipo dado?


He estado intentando crear un tipo que consista en las claves del tipo T cuyos valores sean cadenas. En pseudocódigo sería keyof T donde T[P] es una cadena.

La única manera en que puedo pensar en hacer esto es en dos pasos:

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

Sin embargo, el compilador de TS está diciendo que Key<T> es simplemente igual a keyof T, aunque he filtrado las claves cuyos valores no son cadenas configurándolas en never usando un tipo condicional.

Así que todavía está permitiendo esto, por ejemplo:

interface Thing {
    id: string;
    price: number;
    other: { stuff: boolean };
}

const key: Key<Thing> = 'other';

cuando el único valor permitido de key realmente debería ser "id", no "id" | "price" | "other", ya que los valores de las otras dos claves no son cadenas.

Enlace a un ejemplo de código en el playground de TypeScript

Respuesta #1

Hay una solicitud de función en microsoft/TypeScript#48992 para soportar esto de manera nativa. Hasta que eso se implemente, sin embargo, puedes hacer tu propia versión de varias maneras.

Una manera es con tipos condicionales y tipos de acceso indexado, así:

type KeysMatching<T, V> = {[K in keyof T]-?: T[K] extends V ? K : never}[keyof T];

y luego sacas las claves cuyas propiedades coinciden con string así:

const key: KeysMatching<Thing, string> = 'other'; // ERROR!
// '"other"' is not assignable to type '"id"'

En detalle:

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'

Tenga en cuenta lo que estaba haciendo:

type SetNonStringToNever<T> = { [P in keyof T]: T[P] extends string ? T[P] : never };

realmente solo estaba convirtiendo los valores de propiedades que no son cadenas en valores de propiedad never. No estaba tocando las claves. Tu Thing se convertiría en {id: string, price: never, other: never}. Y las claves de eso son las mismas que las claves de Thing. La principal diferencia con eso y KeysMatching es que deberías estar seleccionando claves, no valores (así que P y no T[P]).

Enlace de Playground al código

Respuesta #2

Como respuesta complementaria:

Desde la versión 4.1 puedes aprovechar el reasignamiento de claves para una solución alternativa (ten en cuenta que la lógica central no difiere de la respuesta de jcalz). Simplemente filtra las claves que, cuando se usan para indexar el tipo de origen, no producen un tipo asignable al tipo de destino y extrae la unión de las claves restantes con 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 justamente mencionó Michal Minich:

Ambos pueden extraer la unión de claves de cadena. Sin embargo, cuando deberían usarse en situaciones más complejas, como T extends Keys..., entonces TS no es capaz de "entender" bien tu solución.

Debido a que el tipo anterior no se indexa con keyof T y en su lugar utiliza keyof del tipo mapeado, el compilador no puede inferir que T se puede indexar por la unión de salida. Para asegurarle esto al compilador, se puede intersectar este último con 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
}

Patio de juegos actualizado

Artículos relacionados