【问题标题】:In TypeScript, how to get the keys of an object type whose values are of a given type?在 TypeScript 中,如何获取值属于给定类型的对象类型的键?
【发布时间】:2019-06-28 11:04:15
【问题描述】:

我一直在尝试创建一个由 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&lt;T&gt; 简单地等于 keyof T,尽管我已经通过使用条件类型将它们设置为 never 过滤掉了值不是字符串的键。

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

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

【问题讨论】:

标签: typescript generics typescript-generics mapped-types


【解决方案1】:

这可以通过conditional typesindexed 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 };

实际上只是将非字符串属性 values 转换为 never 属性值。它没有触摸按键。您的 Thing 将变为 {id: string, price: never, other: never}。其中的键与Thing 的键相同。与 KeysMatching 的主要区别在于您应该选择键,而不是值(所以 P 而不是 T[P])。

Playground link to code

【讨论】:

  • 这似乎有效!您能否简要解释一下为什么这有效而我的尝试无效?
  • 好吧,这是有道理的,但我不确定我自己是否会想出这个。谢谢@jcalz!
  • 这很完美!你能解释一下它是如何工作的吗?谢谢!
  • 你能详细解释一下这段代码是如何工作的吗?我最近开始使用打字稿,我知道基础知识,但不知道这类东西的高级组合。谢谢!
  • @jcalz 这很漂亮。谢谢。
【解决方案2】:

作为补充回答:

从 4.1 版开始,您可以利用 key remapping 作为替代解决方案(请注意,核心逻辑与 jcalz 的 answer 没有区别)。只需过滤掉用于索引源类型时不生成可分配给目标类型的类型的键,然后提取剩余键与 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 正确提到的:

两者都可以提取字符串键的并集。然而,当它们应该用于更复杂的情况时——比如 T extends Keys... 那么 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
}

Updated Playground

【讨论】:

  • 虽然这个答案和来自 jcalz 的答案生成相同的类型,但来自 jcalz 的那个更好,因为 Typescripts 记得生成的键来自原始对象 T 并且可以稍后用于索引它,如 T[K] .有了这个答案,TS 4.3 不知道并发出错误“Type 'K' cannot be used to index type 'T'.ts(2536)”
  • 在 TS 4.3 中?我以为我们几乎没有 4.2.3(可能只是一个错字) Re: indexing - 我在这里遗漏了什么吗 (tsplay.dev/m3Aabw),你能添加一个指向 Playground 的链接来看看吗?
  • 请查看tsplay.dev/mxozbN 它显示了两种解决方案的用法(来自 jcalz 和你的)。两者都可以提取字符串键的并集。然而,当它们应该用于更复杂的情况时——比如T extends Keys...&lt;T, X&gt;,那么 TS 就不能很好地“理解”你的解决方案。也许这是值得报告的错误。我注意到 jcalz 解决方案在 nightly 中表现得更好,它可以了解用于和对象的计算键的属性类型是什么。
  • @MichalMinich - 谢谢,我以为你再也回不去了 :) 今天会仔细看看,但似乎使用 keyof T 进行索引确实让编译器知道生成的类型是keyof T 而在我的版本中,由于使用了映射类型的keyof,编译器不知道这一点。看起来不像一个错误,我理解其背后的逻辑。我看到了两种方法。首先是与keyof T相交以保证编译器。第二个 - Extract&lt;[our type], keyof T&gt;。两者都有效,我会修改答案
  • ^ 但后者显然更冗长,几乎没有好处,所以我会坚持使用keyof T 交集——它更优雅。但这可能太多了keyofs :)
猜你喜欢
  • 2022-09-30
  • 2021-08-06
  • 2023-01-16
  • 1970-01-01
  • 2019-05-08
  • 1970-01-01
相关资源
最近更新 更多