在 TypeScript 中,如何获取值为给定类型的对象类型的键?

In TypeScript, how to get the keys of an object type whose values are of a given type?

我一直在尝试创建一个类型,该类型由 T 类型的键组成,其值为字符串。在伪代码中它将是 keyof T where T[P] is a string.

我能想到的唯一方法是分两步:

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

但是 TS 编译器说 Key<T> 简单地等于 keyof T,即使我通过将它们设置为 [=16= 过滤掉了值不是字符串的键] 使用条件类型。

所以它仍然允许这样,例如:

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

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

key 的唯一允许值实际上应该是 "id",而不是 "id" | "price" | "other",因为其他两个键的值不是字符串。

Link to a code sample in the TypeScript playground

这可以用 conditional types and indexed access types 来完成,像这样:

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

然后你像这样取出属性匹配 string 的键:

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

详细:

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'

注意你在做什么:

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

实际上只是将非字符串 属性 转换为 never 属性 值。它没有触及按键。你的 Thing 会变成 {id: string, price: never, other: never}。那个的键和Thing的键是一样的。它和 KeysMatching 的主要区别是您应该选择键,而不是值(所以 P 而不是 T[P])。

Playground link to code

作为补充回答:

从 4.1 版开始,您可以利用 key remapping for an alternative solution (note that core logic does not differ from jcalz's )。简单地过滤掉那些在用于索引源类型时不会产生可分配给目标类型的类型的键,并提取剩余键与 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

Playground


正如 Michal Minich 正确提到的那样:

Both can extract the union of string keys. Yet, when they should be used in more complex situation - like T extends Keys...<T, X> then TS is not able to "understand" your solution well.

因为上面的类型不使用 keyof T 进行索引,而是使用映射类型的 keyof,编译器无法推断 T 可由输出联合索引。为了确保编译器知道这一点,可以将后者与 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
}

Updated Playground