• 你知道TypeScript 中的 keyof 运算符的用法吗?
  • 发布于 3天前
  • 20 热度
    0 评论
在这篇文章中,我们将深入了解 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;
};
// 堆代码 duidaima.om
// 使用 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 代码中的重要性的相关信息。
用户评论