深入理解 TypeScript 中的 Keyof 运算符,让你的代码更安全、更灵活!

7848047cbb14ac727750581783b4e40b.jpeg

在这篇文章中,我们将深入了解 TypeScript 中的 keyof 运算符。类似于 JavaScript 中的 object.keys 方法,keyof 运算符在 TypeScript 中有着相似的概念。尽管它们在功能上有相似之处,keyof 仅在类型层面上工作并返回一个文字联合类型,而 object.keys 则返回值。keyof 运算符接受一个对象类型并返回其键的字符串或数字文字联合类型。

keyof 运算符是在 TypeScript 2.1 版本中引入的。这个关键字已经成为 TypeScript 中高级类型的基石,并在代码中经常使用。它被称为索引查询运算符,因为该关键字会查询 keyof 后指定的类型。索引基类型查询从属性及其相关元素(如默认关键字及其数据类型)中获取值和属性。

一、如何定义 KeyOf 运算符

在 TypeScript 中,keyof 运算符用于获取用户定义的值。它主要用于泛型,格式类似于联合运算符及其属性。keyof 运算符会检索用户指定的值的索引。这种运算符可以用于如集合和类等对象,通过键值对来存储和检索数据。使用 map 实例对象的 object.keys() 方法,我们可以获取存储在内存中的键。

实例代码解析

让我们通过一个示例代码来更直观地理解 keyof 运算符的用法:

class DemoClass {
    // 定义示例属性
    name: string;
    age: number;
    location: string;
}

// 使用 var 或 let 定义变量,并使用 keyof 关键字
var variableName: keyof DemoClass;
variableName = "name"; // 示例赋值

let anotherVariableName: keyof DemoClass;
anotherVariableName = "age"; // 示例赋值

在上面的代码片段中,我们创建了一个名为 DemoClass 的类,并定义了三个属性:name、age 和 location。随后,我们使用 var 或 let 定义了两个变量 variableName 和 anotherVariableName,并使用 keyof 关键字调用 DemoClass。当我们为变量赋值时,TypeScript 会确保赋值的值是 DemoClass 的有效属性之一。

二、在泛型中使用 KeyOf 运算

使用 KeyOf 运算符应用约束

在 TypeScript 中,keyof 运算符常用于在泛型函数中应用约束。让我们通过一个例子来详细了解这种用法:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

上面的函数使用了泛型来定义一个对象属性的类型。keyof T 返回的是字符串字面量类型的联合。字面量指的是赋值给常量变量的固定值。由于 K 是一个字符串字面量类型,我们使用 extends 关键字对 K 进行约束。索引操作符 obj[key] 返回属性所具有的相同类型。

实际应用

现在我们来看一下 getProperty 函数在实际代码中的使用:

type Staff = {
    name: string;
    empCode: number;
};

const manager: Staff = {
    name: 'Brian',
    empCode: 100,
};

const nameType = getProperty(manager, 'name');  // 返回类型为 string
const empCodeType = getProperty(manager, 'empCode');  // 返回类型为 number
// const invalidType = getProperty(manager, 'sal');  // 编译错误

编译器会验证传递的键是否匹配类型 T 的属性名,因为我们对第二个参数应用了类型约束。如果我们尝试传递一个无效的键,比如 sal,编译器会报错。

手动定义联合类型

在不使用 keyof 运算符时,我们也可以手动定义联合类型:

type keyProp = 'name' | 'empCode';

function getProperty<T, K extends keyProp>(obj: T, key: K): T[K] {
    return obj[key];
}

尽管这种手动方式应用了相同类型的约束,但这种方法的可维护性较差。类型定义会重复,如果原始类型发生变化,手动定义的类型不会自动更新。

三、 KeyOf 与映射类型的结合使用

在 TypeScript 中,我们可以使用 keyof 运算符与映射类型结合,将现有类型转换为新类型。映射类型基于索引签名,通过迭代键来定义尚未声明的属性类型。

示例解析

以下是使用 OptionsFlags 映射类型转换 FeatureFlags 类型的例子:

type OptionsFlags<T> = {
    [Property in keyof T]: boolean;
};

// 使用 OptionsFlags
type FeatureFlags = {
    readingMode: () => void;
    loggedUserProfile: () => void;
};

type UpdatedFeatures = OptionsFlags<FeatureFlags>;

输出结果:

type UpdatedFeatures = {
    readingMode: boolean;
    loggedUserProfile: boolean;
}

在上面的代码片段中,OptionsFlags 被定义为一个包含类型参数 T 的泛型类型。[Property in keyof T] 定义了对类型 T 的属性名称的迭代,方括号表示索引签名语法。因此,OptionsFlags 会将所有 T 类型的属性值重新映射为 boolean 类型。

应用场景

映射类型在实际开发中非常有用,尤其是在需要根据某种规则批量修改类型结构时。例如:

将所有属性设置为可选:

type Partial<T> = {
    [P in keyof T]?: T[P];
};

将所有属性设置为只读:

type Readonly<T> = {
    [P in keyof T]: readonly T[P];
};

四、 KeyOf 运算符与显式键

使用 KeyOf 运算符创建联合类型

在 TypeScript 中,当我们在具有显式键的对象类型上使用 keyof 运算符时,它会创建一个联合类型。下面是一个具体的例子:

interface User {
    userName: string;
    id: number;
}

function userData(user: User, property: keyof User) {
    console.log(`Print user information ${property}: "${user[property]}"`);
}

let user = {
    userName: "Karl",
    id: 100
};

userData(user, "userName");

输出结果

Print user information userName: "Karl"

在上面的代码中,我们定义了一个 User 接口和一个 userData 函数。函数接受一个 User 对象和一个 User 类型的属性键,并打印相应的用户信息。

应用场景

keyof 运算符在实际开发中有很多应用场景,特别是在处理动态属性访问和确保类型安全时。例如:

  • 动态访问对象属性 : 使用 keyof 可以确保我们访问的属性在对象上是有效的,从而避免运行时错误。

  • 类型安全的配置对象: 当我们处理配置对象时,可以使用 keyof 来确保配置项的名称是预定义的有效值。

通过在对象类型上使用 keyof 运算符,我们可以创建联合类型,从而确保属性访问的类型安全性。这种方式不仅提高了代码的可读性和维护性,还减少了潜在的错误。

五、索引签名与 KeyOf 运算符

在 TypeScript 中,keyof 运算符可以与索引签名一起使用,以移除索引类型。索引签名用于表示对象的类型,其中对象的值是一致的类型。

示例解析

以下是一个使用 keyof 运算符与索引签名的例子:

type stringMapDemo = {[key: string]: unknown};

function sampleStringPair(property: keyof stringMapDemo, value: string): stringMapDemo {
    return {[property]: value};
}
  • 我们定义了一个类型 stringMapDemo,它表示一个对象,其中所有键都是字符串类型,所有值的类型为 unknown。

  • 函数 sampleStringPair 接受两个参数:property(类型为 keyof stringMapDemo)和 value(字符串类型),并返回一个 stringMapDemo 类型的对象。通过使用 keyof stringMapDemo,我们确保传递的 property 是一个字符串类型的键。

六、使用 KeyOf 条件映射类型

条件类型用于根据条件表达式在两个声明的类型之间进行选择。结合使用 keyof 和 TypeScript 映射类型,我们可以进行条件类型映射,从而更灵活地定义类型。

示例

以下是一个使用条件类型和 keyof 进行条件映射的例子:

type OptionsFlags<T> = {
    [Property in keyof T]: T[Property] extends Function ? T[Property] : boolean
};

type DemoFeatures = {
    readingMode: () => void;
    loggedUserProfile: () => void;
    loginPassword: string;
    userName: string;
};

type Features = OptionsFlags<DemoFeatures>;

运行后 Features 的类型结构如下

type Features = {
    readingMode: () => void;
    loggedUserProfile: () => void;
    loginPassword: boolean;
    userName: boolean;
}

代码解析

  • 在 OptionsFlags 类型中,我们使用条件类型 T[Property] extends Function ? T[Property] : boolean 来决定每个属性的类型。如果属性是函数类型,则保持不变;否则,将其映射为 boolean 类型。

  • DemoFeatures 类型包含了两个方法(readingMode 和 loggedUserProfile)和两个字符串属性(loginPassword 和 userName)。

  • 我们使用 OptionsFlags 来定义新类型 Features。通过条件映射,Features 类型中的方法保持不变,而字符串属性被映射为 boolean 类型。

应用场景

条件映射类型在处理复杂类型转换时非常有用,尤其是当我们需要根据属性类型进行动态转换时。例如:

  • 动态类型转换: 根据属性类型动态决定新类型,可以用于配置、表单验证等场景。

  • 类型安全的属性转换: 通过条件映射类型,我们可以确保类型转换的安全性,并自动反映类型的变化。

七、使用 Keyof 和 Utility Types

实用类型是一组内置的映射类型,可以帮助我们简化和重构类型定义。下面我们来看几个使用 keyof 和实用类型的例子。

Record 类型

Record 是 TypeScript 提供的实用类型,用于将所有属性键映射到指定的类型 T。

type Record<K extends keyof any, T> = {
    [P in K]: T;
};

示例

假设我们有一个 FeatureFlags 类型:

type FeatureFlags = {
    readingMode: () => void;
    loggedUserProfile: () => void;
    loginPassword: string;
    userName: string;
};

我们可以使用 Record 实用类型将所有属性映射为 boolean 类型:

type Features = Record<keyof FeatureFlags, boolean>;

// 结果类型
type Features = {
    readingMode: boolean;
    loggedUserProfile: boolean;
    loginPassword: boolean;
    userName: boolean;
};

Record 实际应用场景

在这个例子中,我们使用了 TypeScript 的 Record 实用类型来创建一个映射,该映射将 Status 枚举的值映射到具有特定结构的对象。让我们详细解释这个示例。

定义 Status 枚举

首先,我们假设有一个 Status 枚举:

enum Status {
    OPEN = "OPEN",
    STARTED = "STARTED",
    CLOSED = "CLOSED"
}

定义 Props 接口

然后,我们定义了一个接口 Props,其中包含一个 status 属性,其类型为 Status 枚举:

interface Props {
    status: Status;
}

使用 Record 定义 statusMap

接下来,我们使用 Record 实用类型定义了一个 statusMap 对象,该对象将 Status 枚举的每个值映射到一个具有 label 和 color 属性的对象:

const statusMap: Record<Status, { label: string; color: "bg-red-400" | "bg-blue-400" | "bg-green-400" }> = {
    OPEN: { label: "Open", color: "bg-red-400" },
    STARTED: { label: "Started", color: "bg-blue-400" },
    CLOSED: { label: "Closed", color: "bg-green-400" },
};

组件调用

const TicketStatusBadge: React.FC<Props> = ({ status }) => {
    return (
        <Badge className={`
            ${statusMap[status].color} 
            text-background
            hover:${statusMap[status].color}
        `}>
            {statusMap[status].label}
        </Badge>
    );
};

解析

  • Record类型将 Status 枚举的每个值映射到一个对象,该对象具有 label 属性(字符串类型)和 color 属性(特定字符串字面量类型)。

  • statusMap 对象符合 Record 类型定义,确保每个 Status 枚举值都映射到一个具有 label 和 color 属性的对象。

这个模式在实际开发中非常有用,特别是在需要根据某些状态(如枚举)来确定显示样式或标签时。

Pick 类型

Pick 是另一个实用类型,它允许我们从一个对象类型中选择一个或多个属性,并生成一个包含这些属性的新类型。

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

示例

假设我们有一个 User 类型:

type User = {
    id: number;
    name: string;
    age: number;
    email: string;
};

我们可以使用 Pick 类型选择 id 和 name 属性:

type UserPreview = Pick<User, 'id' | 'name'>;

// 结果类型
type UserPreview = {
    id: number;
    name: string;
};

在这个例子中,UserPreview 类型只包含 id 和 name 属性。

通过使用 TypeScript 的实用类型,如 Record 和 Pick,我们可以轻松地重构和简化类型定义。结合 keyof 运算符,我们可以确保类型的灵活性和安全性。

结束

TypeScript 的 keyof 运算符虽然小巧,但却是 TypeScript 机制中不可或缺的一环。当我们将 keyof 与 TypeScript 的其他工具结合使用时,可以提供良好的类型约束,从而提升代码的类型安全性。

keyof 类型注解用于提取对象的键。通过 object.keys() 方法,我们可以检索键的索引及其值。在处理企业级应用程序时,用户可以轻松地检索数据。

在本文中,我们探讨了如何在 TypeScript 泛型、映射类型、显式键、索引签名、条件映射类型和实用类型中使用 keyof 运算符。希望这篇文章能为你提供有关 keyof 关键字及其在 TypeScript 代码中的重要性的相关信息。

如果你觉得本文对你有所帮助,请分享给你的朋友,并在评论区留下你的看法和问题。关注我的公众号「前端达人」,获取更多关于 TypeScript 和其他前沿技术的精彩内容。让我们一起写出更优雅、更健壮的代码!

  • 23
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值