TypeScript 实用程序:keyof 嵌套对象
在这篇博文中,我们将学习如何构建一个 TypeScript 实用类型,它公开对象的所有关键路径,包括嵌套路径。
这有什么用处?
你是否曾经写过一个 TypeScript 函数,通过指定对象及其属性的路径来接收该对象的特定属性?像这样:
const person = {
name: "John",
age: 30,
dog:{
name: "Rex",
}
}
function get<ObjectType>(object: ObjectType, path: string){
const keys = path.split('.');
let result = object;
for (const key of keys) {
result = result[key];
}
return result;
}
get(person, "dog.name") // Rex
嗯,显然这很好用,但你并没有充分利用 TypeScript!你很容易在第二个参数(路径)上写错,并在调试时丢失一些宝贵的类型信息。
那么 TypeScript 可以如何帮助我们呢?
不幸的是,目前还没有原生的工具类型能够提供嵌套对象内的所有关键路径。但如果你的对象只有一层深度,TypeScript 的keyof
运算符就足够了!
const person = {
name: "John",
age: 30,
job: "Programmer"
}
function get<ObjectType>(object: ObjectType,
path: keyof ObjectType & string){
...
}
这样,您将拥有一个真正的类型安全函数,它只允许您添加"name"
或"age"
作为"job"
第二个参数。
如果您不理解我上面展示的一些技术细节,请继续阅读,我将在下面更详细地解释。
深度超过 1 级的对象
现在,对于深度超过 1 级的对象来说,keyof
这还远远不够,您可能已经意识到了这一点。
在进入 TypeScript 的实现细节之前,让我们尝试思考一种算法,该算法可以让我们获取具有 N 级深度的对象的所有键。
- 遍历对象的键
- 如果键的值不是对象,那么它就是一个有效的键
- 否则,如果键是一个对象,则连接该键并返回步骤 1
有了这个算法,以及这些“简单”的编程原理、循环语句、条件和递归,这似乎并不那么难!
现在,让我们采用该算法并构建一个 JS 函数,该函数可以提取任何给定对象中所有节点的所有键。
const objectKeys = [];
const person = {
name: 'pfigueiredo',
age: 30,
dog: {
owner: {
name: 'pfigueiredo'
}
}
};
function getObjectKeys(obj, previousPath = '') {
// Step 1- Go through all the keys of the object
Object.keys(obj).forEach((key) => {
// Get the current path and concat the previous path if necessary
const currentPath = previousPath ? `${previousPath}.${key}` : key;
// Step 2- If the value is a string, then add it to the keys array
if (typeof obj[key] !== 'object') {
objectKeys.push(currentPath);
} else {
objectKeys.push(currentPath);
// Step 3- If the value is an object, then recursively call the function
getObjectKeys(obj[key], currentPath);
}
});
}
getObjectKeys(person); // [ 'name', 'age', 'dog', 'dog.owner', 'dog.owner.name' ]
因此,我们知道如何以编程方式执行此操作,现在的目标是尝试将相同类型的概念与 TypeScript 现有的运算符和实用程序类型应用到构建一个generic type
将对象的所有键作为文字类型提供给我们的对象。
创建 TypeScript 实用程序类型
我们将在下面创建的实用程序类型只有在 TypeScript 4.0版本发布后才有可能,因为它引入了文字类型。
在本节中,我们将逐步介绍如何创建能够提取任何给定对象内的所有键的 TypeScript 实用程序类型。
类型定义
创建此实用程序的第一步显然是声明一个新的 TypeScript 类型并赋予它一个名称:
1-声明新类型
type NestedKeyOf = {};
下一步是让这个类型变得“通用”,也就是说,它应该接受我们传入的任何对象。TypeScript
已经嵌入了这个通用特性,它允许我们创建一个灵活的、可以接受任何对象的方法。
2-接受泛型类型参数
type NestedKeyOf<ObjectType> = {};
// using
type ObjectKeys = NestedKeyOf<Person>;
单独添加泛型类型参数本身并不会限制传入实用程序的类型。为此,我们需要添加extends
关键字,以便仅接受对象类型——任何遵循“键值”对数据类型的类型。
3-约束泛型参数
type NestedKeyOf<ObjectType extends object> = {};
太好了,我们已经定义了类型的签名,现在我们需要做“真正的工作”,即进行实现。
类型实现
回到我们的算法,创建这个实用程序的第一步是“遍历对象的键”。TypeScript 使用一种称为“映射类型”的技术简化了这一过程,它是一种遍历对象键并根据每个键设置值类型的方法。
1-遍历对象的键
// Create an object type from `ObjectType`, where the keys
// represent the keys of the `ObjectType` and the values
// represent the values of the `ObjectType`
type NestedKeyOf<ObjectType extends object> =
{[Key in keyof ObjectType]: ObjectType[Key]};
现在我们能够遍历所有对象的键并使用它们来访问对象的每个值,我们可以继续执行算法的第二步:“如果键的值不是对象,那么它就是有效键”。
我们将利用 TypeScript 的条件类型来进行检查,其工作原理如下:
// Take a `Type`, check if it "extends" `AnotherType`
// and return a type based on that
type Example = Dog extends Animal ? number : string;
2-检查它是否是有效密钥
// If the value is NOT of type `object` then
// set it as the generated object's value type
type NestedKeyOf<ObjectType extends object> =
{[Key in keyof ObjectType]: ObjectType[Key] extends object
? "" /*TODO*/
: Key
};
// But we want what's under the object's values,
// so we need to access it
type NestedKeyOf<ObjectType extends object> =
{...}[keyof ObjectType];
type Person = {
name: 'pfigueiredo',
age: 30,
dog: {
owner: {
name: 'pfigueiredo'
}
}
};
NestedKeyOf<Person>; // "name" | "age" | ""
因此,我们现在可以访问对象的所有第一级键,但显然我们仍然缺少指向其他级别属性的路径,例如dog.owner
和dog.owner.name
。
为了实现这一点,我们应该遵循算法的第 3 步:“否则,如果键是一个对象,则连接该键并返回到步骤 1。”
为了实现这一点,我们需要使用 TypeScript 的递归类型,它实际上像任何其他编程语言一样工作 - 具有调用调用该条件的相同“类型”(递归)的条件,并且具有导致实际结果的条件。
3 -添加类型递归
// 1 - If it's an object, call the type again
type NestedKeyOf<ObjectType extends object> =
{[Key in keyof ObjectType]: ObjectType[Key] extends object
? NestedKeyOf<ObjectType[Key]>
: Key
}[keyof ObjectType];
// 2 - Concat the previous key to the path
type NestedKeyOf<ObjectType extends object> =
{[Key in keyof ObjectType]: ObjectType[Key] extends object
? `${Key}.${NestedKeyOf<ObjectType[Key]>}`
: Key
}[keyof ObjectType];
// 3 - Add the object's key
type NestedKeyOf<ObjectType extends object> =
{[Key in keyof ObjectType]: ObjectType[Key] extends object
? `${Key}` | `${Key}.${NestedKeyOf<ObjectType[Key]>}`
: Key
}[keyof ObjectType];
基本上就是这样,这个NestedKeyOf
实用程序类型应该已经能够提取具有任何给定深度的对象的所有可能的属性路径,但是 TypeScript 可能仍然会因为在文字中使用非字符串/数字而对你大喊大叫,让我们解决这个问题!
为了仅选择特定类型的键,我们需要利用交叉类型,这只是使用运算符的问题&
。
4-仅提取字符串/数字键
// add `& (string | number)` to the keyof ObjectType
type NestedKeyOf<ObjectType extends object> =
{[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
? `${Key}` | `${Key}.${NestedKeyOf<ObjectType[Key]>}`
: `${Key}`
}[keyof ObjectType & (string | number)];
使用 NestedKeyOf 进行排序的示例
现在我们已经完成了 TypeScript 实用程序类型的实现,现在是时候看一个简单的示例了,它在你可能正在工作的任何项目中都非常有用👇
通过在函数中使用此实用程序sortBy
,我们可以安全地选择对象的某个属性,并确保我们没有输入任何错误,并始终与对象的结构以及我们传递的内容保持同步🤯
概括
- 创建一个接受泛型的类型
- 将泛型约束为对象
- 借助映射类型创建一个新对象
- 对于每个键,检查值是对象还是原始类型
- 如果它是一个对象,则连接当前键并以递归方式调用该类型
- 仅查找字符串和数字键
顺便提一下,我要感谢David Sherret,他发布了一个Stack Overflow 答案,看起来有点像我上面描述的实用程序类型🙏
鏂囩珷鏉ユ簮锛�https://dev.to/pffigueiredo/typescript-utility-keyof-nested-object-2pa3