在TypeScript中,如何仅指定值为字符串的泛型对象的键?

Aro*_*ron 4 generics typescript typescript-generics mapped-types

我一直在尝试创建一个由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>;
Run Code Online (Sandbox Code Playgroud)

然而,TS编译器说这Key<T>简直等于keyof T,即使我通过将它们设置为never使用条件类型来过滤掉其值不是字符串的键.

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

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

const key: Key<Thing> = 'other';
Run Code Online (Sandbox Code Playgroud)

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

链接到TypeScript playground中的代码示例

jca*_*alz 14

这可以使用条件类型查找类型来完成,如下所示:

type KeysMatching<T, V> = {[K in keyof T]: T[K] extends V ? K : never}[keyof T];
Run Code Online (Sandbox Code Playgroud)

然后你拉出属性匹配的键,string如下所示:

const key: KeysMatching<Thing, string> = 'other'; // ERROR!
// '"other"' is not assignable to type '"id"'
Run Code Online (Sandbox Code Playgroud)

详细地:

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'
Run Code Online (Sandbox Code Playgroud)

请注意你在做什么:

type SetNonStringToNever<T> = { [P in keyof T]: T[P] extends string ? T[P] : never };
Run Code Online (Sandbox Code Playgroud)

实际上只是将非字符串属性转换为never属性值.它没有碰到钥匙.你Thing会成为{id: string, price: never, other: never}.并且它的键与键的键相同Thing.与此相关的主要区别KeysMatching在于您应该选择键,而不是值(因此P而不是T[P]).

希望有所帮助.祝好运!

  • 这是一个[映射修饰符](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#mapping-modifiers),它删除了所有 props 的可选性 (4认同)
  • “”前面的“-”是什么?? (3认同)
  • 好吧,这是有道理的,尽管不确定我自己是否会想到这一点。谢谢@jcalz! (2认同)

Ole*_*ter 14

作为补充答案:

从 4.1 版开始,您可以利用密钥重映射作为替代解决方案(请注意,核心逻辑与 jcalz 的答案没有区别)。简单地过滤掉在用于索引源类型时不会产生可分配给目标类型的类型的键,并使用以下方法提取剩余键的并集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
Run Code Online (Sandbox Code Playgroud)

操场


正如Michal Minich正确提到的:

两者都可以提取字符串键的并集。然而,当它们应该在更复杂的情况下使用时——比如 T extends Keys...<T, X> 那么 TS 无法很好地“理解”你的解决方案。

因为上面的类型不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
}
Run Code Online (Sandbox Code Playgroud)

更新的游乐场

  • 虽然这个答案和 jcalz 的答案生成相同的类型,但 jcalz 的答案更好,因为 Typescripts 会记住生成的键来自原始对象 T,并且可以在以后用于索引它,如 T[K] 中一样。有了这个答案,TS 4.3 不知道这一点,并发出错误“类型'K'不能用于索引类型'T'.ts(2536)” (2认同)