概述
- 在TypeScript中,
extends
关键字是类型系统中一个极其重要的组成部分 - 它不仅用于类的继承,也是类型兼容性检查和泛型约束的关键机制
- 特别是当它与
keyof
关键字结合,形成K extends keyof T
的结构时 - 它为类型系统带来了强大的灵活性和表达能力,让我们能够在泛型中对对象的属性进行操作和约束
K extends keyof T
- 在TypeScript中,当你声明一个泛型约束为
K extends keyof T
时 - 这意味着泛型参数K被限制为只能是T类型上存在的属性名的子集
- 这在处理对象属性、映射类型或者条件类型时非常有用
示例1
interface User {
id: number;
name: string;
email: string;
}
function getProperty<T, K extends keyof T>(user: T, key: K): T[K] {
return user[key];
}
const user = { id: 1, name: "Alice", email: "alice@example.com"};
console.log(getProperty(user, "name")); // 输出 "Alice"
- 在这个例子中,
K extends keyof User
确保了getProperty函数的key参数, 只能是User接口中定义的属性名
示例2 属性全面转化成只读
interface User {
id: number;
name: string;
email: string;
}
type ReadonlyStringFields<T> = {
readonly [P in keyof T]: T[P];
}
type ReadonlyUser = ReadonlyStringFields<User>;
// ReadonlyUser 类型为:
// {
// id: number;
// readonly name: string;
// readonly email: string;
// }
- 这里,ReadonlyStringFields 将所有属性转化为只读,当然
示例3:部分属性只读
type MakeSomePropertiesReadonly<T, K extends keyof T> = {
readonly [P in K]: T[P];
} & {
[P in Exclude<keyof T, K>]: T[P];
};
interface UserInfo {
id: number;
username: string;
email: string;
isAdmin: boolean;
}
type ReadonlyUserDetails = MakeSomePropertiesReadonly<UserInfo, 'id' | 'email'>;
function displayUserInfo(user: ReadonlyUserDetails) {
console.log(`ID: ${user.id}, Email: ${user.email}`);
// 下面这行如果尝试在真实代码中执行,会因为类型检查而在编译时失败
// user.id = 123; // Error: Cannot assign to 'id' because it is a read-only property.
user.username = "NewUsername"; // 这是允许的,因为 username 不是只读的
}
// 假设我们有一个UserInfo实例,为了演示,直接构造一个符合 ReadonlyUserDetails 的对象
const userDetails: ReadonlyUserDetails = {
id: 42,
username: "JohnDoe",
email: "john.doe@example.com",
isAdmin: false,
};
displayUserInfo(userDetails);
-
在 MakeSomePropertiesReadonly<T, K>
中 - 泛型参数:
-
T
: 表示你想要修改属性可读性的原始对象类型 -
K extends keyof T
: 表示一个泛型约束,要求 K 必须是 T 类型的键(即属性名)的一个子集。这意味着你可以指定 T 中任意数量和名称的属性来变为只读
-
- 类型别名结构:
-
{ readonly [P in K]: T[P]; }:
这部分创建了一个新类型,其中 K 集合内的每个属性 P 被声明为只读。[P in K] 是一个映射类型,遍历 K 中的所有键,并为每个键创建一个属性,其值类型与 T[P] 相同,但加上了 readonly 修饰符。 -
& { [P in Exclude<keyof T, K>]: T[P]; }
这部分用来保留 T 类型中未被指定为只读的那些属性。Exclude<keyof T, K>
是一个实用类型,用于从 T 的所有键中排除已经在 K 中的键,确保剩余的属性不被重复定义且保持原样。
-
K extends keyof any
- 当K extends keyof any时,这个约束实际上没有起到任何限制作用
- 因为any类型在TypeScript中是最宽泛的类型,表示可以代表任何类型,所以任何类型都可以被认为是any的键
- 这通常在你想要泛型参数可以是任何类型时使用,但这种用法在实践中较少见,因为失去了类型安全性的优势
示例
function getProperty<K extends keyof any>(obj: any, key: K): any {
return obj[key];
}
const person = {
name: 'Alice',
age: 30,
address: '123 Main Street'
};
// 由于使用了 keyof any,我们可以传入任何类型的键
const name = getProperty(person, 'name'); // string
const age = getProperty(person, 'age'); // number
const address = getProperty(person, 'address'); // string
const unknownProp = getProperty(person, 'unknownProp'); // undefined,但不会引发类型错误
console.log(name); // 输出: Alice
console.log(age); // 输出: 30
console.log(address); // 输出: 123 Main Street
console.log(unknownProp); // 输出: undefined
-
keyof any
表示任何可能的属性名,因为any
类型可以包含任意属性 - 使用
K extends keyof any
实际上对K
没有太多限制,它可以是任意字符串或符号 - 这种约束通常不是很有用,因为它不提供关于K具体可能是什么的明确信息
K extends keyof (string | number | symbol)
- 这个表达式意味着泛型参数K可以是string或symbol类型中任何一个的键名
- 在TypeScript中,对象的键通常是字符串或符号类型,但不包括数字(除非使用了计算属性名)
- 因此,这个约束在直觉上可能用于处理特殊情况,比如当你知道泛型参数可能被用于索引一个映射到字符串或符号属性上,但实际上在标准对象操作中,数字作为键的用法不常见
示例
type AcceptableKeys = string | number | symbol;
function processKey<K extends AcceptableKeys>(key: K): void {
console.log(`Processing key: ${key.toString()}`);
}
// 使用字符串作为键
processKey("myStringKey");
// 使用数字作为键
processKey(123);
// 使用符号作为键
const mySymbol = Symbol("mySymbol");
processKey(mySymbol);
- 在这个示例中,我们定义了一个类型别名
AcceptableKeys
,它表示可以接受的键类型是字符串、数字或符号 - 然后,我们定义了一个泛型函数
processKey
,它接受一个类型为 K 的参数,其中 K 被约束为必须扩展(extends)AcceptableKeys
- 这样,我们就可以向
processKey
函数传递字符串、数字或符号类型的参数