TypeScript 中一个常见的问题是,当你想获取一个对象的键值时,该对象的值是给定的类型,例如,假设你有一个这样的对象:
type Obj = {
a: string;
b: string;
c: number;
d: number;
};
我们只想检索值为 string
的键,所以我们想得到 "a" | "b"
。
这看起来很棘手,但让我们试一试。
快速解决方案
这种技术使用键重映射来只提取字符串键:
你可以提供一个通用的条件来让它可重用:
解释
让我们看看我是如何得出这个解决方案的,我会试着带你走过我的思考过程。
如果你把这些泛型类型看作有开头、中间和结尾,那么它们就更容易理解。
在开始时,我们从一个简单的对象开始。
type Obj = {
a: string;
b: string;
c: number;
d: number;
};
我们知道我们正在朝向键的 union 方向前进,所以我们知道 keyof
将被包含在某个地方:
type KeysOfObj = keyof Obj;
// 'a' | 'b' | 'c' | 'd'
你可能会想,太棒了,我们已经完成了一半,现在我们需要做的就是排除没有字符串值的键,也许可以使用 Exclude
类型的帮助器:
当我们尝试填充 Exclude
的第二个类型参数时,问题变得明显起来,我们需要当前键的上下文来确定 Obj[Key]
是否是一个字符串。
所以我们类型的“中间”将是关于遍历每个键。整个故事很清楚——让我们实现它。
遍历每个键
有几种不同的方法可以实现这个目标,我最喜欢的是使用IIMT——即立即索引映射类型。
type Obj = {
a: string;
b: string;
c: number;
d: number;
};
type KeysOfObj = {
// ^?
[K in keyof Obj]: `hello_${K}`;
}[keyof Obj];
这让我们可以使用映射类型创建一个对象,然后使用 keyof Obj
索引到它,以输出我们创建的对象的值。
这样做的好处是,我们可以访问当前键和同一作用域中的对象,因此我们可以使用当前键来索引对象。
在本例中,我们使用它创建一个字符串字面量类型,每个键都带有 hello_
前缀。
但是如果我们想根据值的类型做一些不同的事情呢?
条件类型
我们可以使用条件类型来完成这项工作。条件类型允许你用条件表达if/else逻辑:
type Obj = {
a: string;
b: string;
c: number;
d: number;
};
type Tests = [
// ^?
Obj["a"] extends string ? true : false,
Obj["b"] extends string ? true : false,
Obj["c"] extends string ? true : false,
Obj["d"] extends string ? true : false
];
在本例中,我们检查当前键的值是否为字符串,如果不是,则返回 never
,而不是 false
。
你会注意到,从技术上讲, KeysOfObj
的类型应该是 'a' | 'b' | never | never
:
type Obj = {
a: string;
b: string;
c: number;
d: number;
};
type Tests = [
// ^?
Obj["a"] extends string ? "a" : never,
Obj["b"] extends string ? "b" : never,
Obj["c"] extends string ? "c" : never,
Obj["d"] extends string ? "d" : never
];
但是 TypeScript 会自动从联合中移除 never
,所以我们最终得到的是 'a' | 'b'
。
使其通用
最后的挑战是将这个转换为一个可以用于任何对象的类型帮助器,我们可以通过给 StringKeys
添加一个类型参数并删除对 Obj
的硬编码引用来实现:
为了让它更具可重用性,我们可以给助手函数添加第二个类型参数,以便指定条件:
我们已经找到了我们的类型帮助器,我们接收一个对象,通过IIMT来遍历它的键,并使用条件类型来检查当前键的值是否与条件匹配。
欢迎关注公众号:文本魔术,了解更多