概述
- in和- keyof是两个非常重要的操作符,它们允许开发者对对象的键(key)进行更精细化的操作和约束
- in 关键词 - in关键词则在TypeScript的类型上下文中有特定的用途,它用于映射类型和类型查询
- 当与keyof一起使用时,in可以遍历一个类型的所有键,并在类型层次上进行操作
- 比如创建新的类型映射类型、条件判断或是泛型约束
 
- keyof 关键词 - keyof T是一个类型查询操作符,其中T是一个类型
- 它返回一个联合类型,该联合类型包含了T所有公开属性的键(即属性名)
- 这在TypeScript中非常重要,因为它允许我们动态地基于一个类型来获取它的属性名集合
- 从而进行进一步的类型操作
 
- 应用场景 - 动态属性访问:在处理对象时,利用K in keyof T可以确保安全地遍历所有可能的属性,而不会引发运行时错误
- 类型转换:Readonly类型别名,将所有属性转为只读,是映射类型的一种经典应用
- API设计:在API设计中,通过K extends keyof约束泛型参数,可以精确地指定参数必须是对象的某个属性,增强了API的类型安全和易用性
- 配置管理:动态配置对象时,利用映射类型和条件类型可以方便地实现配置项的默认值设定、类型转换等,保持代码的灵活性和可维护性
 
in 操作符
- 在 TypeScript 中,in 操作符主要在类型守卫(type guard)和映射类型(mapped type)的上下文中使用
1 ) 类型守卫中的 in
- 在类型守卫中,in 用于检查一个对象是否具有特定的属性
- 这有助于在运行时确定对象的形状,并据此进行条件处理
示例
function processObject(obj: any) {  if ('name' in obj && 'age' in obj) {  console.log(`Name: ${obj.name}, Age: ${obj.age}`);  } else {  console.log('Object does not have name and age properties.');  }  
}
- 在这个例子中,我们使用 in 操作符来检查 obj 是否具有 name 和 age 属性
2 )映射类型中的 in
- 在映射类型中,in 用于遍历一个类型的所有属性键,并对每个属性进行转换
- 这允许开发者创建一个新的类型,该类型基于现有类型的每个属性进行某种变换
- 例如,我们可以定义一个映射类型,将 Person 类型的所有属性都变为可选:
示例
type PartialPerson = { [K in keyof Person]?: Person[K] };
- 在这个例子中,PartialPerson 类型将 Person 类型的所有属性都标记为可选
- 这是通过使用映射类型并结合 keyof 和 in 操作符来实现的
- 对于 Person 类型的每一个键 K,我们都定义了一个新的可选属性
- 其类型与原始 Person 类型中对应属性的类型相同
keyof 操作符
- keyof 操作符用于获取对象类型的所有键(key)的联合类型(union type)
- 这意味着,如果你有一个对象类型
- 你可以使用 keyof 来得到一个包含该对象所有键名称的字符串字面量类型的联合
示例
type Person = {  name: string;  age: number;  address: {  city: string;  state: string;  };  
};  type PersonKeys = keyof Person; // "name" | "age" | "address"
- 在这个例子中,PersonKeys 类型是一个联合类型
- 包含了 “name”, “age”, 和 “address” 这三个字符串字面量类型
in 和 keyof 联合
- in 和 keyof 可以结合使用,以实现更复杂的类型操作
- 例如,你可以定义一个类型,该类型只包含 Person 中字符串类型的属性
示例
type Person = {    name: string;    age: number;    address: {    city: string;    state: string;    };  isStudent: boolean; // 添加一个非字符串类型的属性作为示例  
};  // 使用映射类型和条件类型来提取字符串类型的属性键  
type StringProps = { [K in keyof Person]: Person[K] extends string ? K : never }[keyof Person];  // 提取出来的 StringProps 类型应该只包含 'name'  
// 因为只有 'name' 属性的类型是 string  // 示例:使用 StringProps 类型  
let stringProp: StringProps;  
stringProp = 'name'; // 正确,因为 'name' 是 Person 类型中唯一的字符串属性  
// stringProp = 'age'; // 错误,因为 'age' 不是字符串类型  
// stringProp = 'isStudent'; // 错误,因为 'isStudent' 也不是字符串类型  // 展示 never 类型的示例  
// 尝试将非字符串类型的属性键赋值给 StringProps 类型的变量会导致编译错误  
type NonStringProp = Exclude<keyof Person, StringProps>; // 这会得到 'age' | 'address' | 'isStudent'  let nonStringProp: NonStringProp;  
nonStringProp = 'age'; // 正确,因为 'age' 是非字符串属性  
nonStringProp = 'isStudent'; // 正确,'isStudent' 也是非字符串属性  
// nonStringProp = 'name'; // 错误,因为 'name' 是字符串类型  // 使用一个辅助类型来尝试将 Person 的每个属性都转换为 StringProps,非字符串类型的属性会被转换为 never  
type AttemptStringConversion<K extends keyof Person> = Person[K] extends string ? K : never;// 示例:'age' 属性不是字符串,所以它的类型是 never  
let ageAsStringProp: AttemptStringConversion<'age'>; // 类型是 never  
// 下面这行代码会导致编译错误,因为我们不能将任何值赋给 never 类型的变量  
// ageAsStringProp = 'any value'; // 错误,不能将类型“string”分配给类型“never”  // 正确的使用方式是,不直接给 never 类型的变量赋值  
// 而是使用类型守卫或其他逻辑来避免处理 never 类型的情况
- 在这个例子中,首先定义了一个映射类型,该类型遍历 Person的所有属性
- 对于每个属性 K,如果其类型是字符串,则保留该属性的键名;否则,将其替换为 never类型
- 然后,我们通过索引这个映射类型与 keyof Person的结果
- 得到了一个包含所有字符串类型属性键的联合类型
- 其他,参考上述注释
基于 in keyof 实现轻量级 Map
class LightweightMap<K extends string | number | symbol, V> {  private data: Record<K, V> = {} as Record<K, V>;  set(key: K, value: V): void {  this.data[key] = value;  }  get(key: K): V | undefined {  return this.data[key];  }  has(key: K): boolean {  return key in this.data;  }  delete(key: K): boolean {  const hadKey = this.has(key);  if (hadKey) {  delete this.data[key];  }  return hadKey;  }  clear(): void {  this.data = {} as Record<K, V>;  }  size(): number {  return Object.keys(this.data).length;  }  // 可选:实现遍历功能  forEach(callback: (value: V, key: K, map: this) => void): void {  for (const key in this.data) {  if (this.data.hasOwnProperty(key)) {  callback(this.data[key], key as K, this);  }  }  }  
}  // 使用示例  
const map = new LightweightMap<string, number>();  
map.set('one', 1);  
map.set('two', 2);  console.log(map.get('one')); // 输出: 1  
console.log(map.has('two')); // 输出: true  map.forEach((value, key, mapInstance) => {  console.log(`Key: ${key}, Value: ${value}`);  
});  map.delete('two');  
console.log(map.has('two')); // 输出: false  console.log(map.size()); // 输出: 1  
map.clear();  
console.log(map.size()); // 输出: 0
- 上述代码定义了一个名为 LightweightMap 的泛型类
- 该类模拟了标准 Map 数据结构的一些基本功能,但是以一种更轻量级的方式实现
- 这个类使用了 TypeScript 的泛型特性来提供类型安全
- 允许用户为键(K)和值(V)指定类型
- 注意事项和改进点 - 性能: 对于大型数据结构,使用 Object.keys(this.data).length 来计算大小可能会影响性能。可以考虑维护一个内部计数器来跟踪大小,以减少计算开销。
- 类型安全: 虽然 LightweightMap 提供了类型安全的键值对存储,但在实际使用中仍需注意确保提供给 set 方法的键是唯一的,以避免意外覆盖现有值。
- 错误处理: 当前实现中没有显式的错误处理机制。在实际应用中,可能需要添加错误处理逻辑来处理例如尝试获取不存在的键或删除不存在的键等情况。
- 扩展性: 根据具体需求,可以考虑为 LightweightMap 类添加更多功能,如 keys()、values()、entries() 等方法,以便更灵活地处理键值对。
 
结论
- in和- keyof是 TypeScript 类型系统中的强大工具
- 它们允许开发者对对象的键进行精细化的操作和约束
- 通过结合使用这两个操作符,开发者可以定义出更加灵活和强大的类型
- 从而提高代码的可读性和健壮性
- in与keyof的结合,是TypeScript类型系统中的精华
- 它们的配合使用极大地拓展了类型系统的边界
- 为开发者提供了前所未有的类型操纵能力
- 通过映射类型、条件判断、泛型约束等功能,开发者能写出更安全、灵活、可维护的代码