泛型
泛型(Generics)是TypeScript中一个非常强大的特性,它允许我们在定义函数、接口或类时不预先指定具体的类型,而是在使用时再指定类型。泛型的核心思想是类型参数化,通过泛型我们可以编写出更加灵活、可复用的代码,同时又不失类型安全性。
为什么需要泛型
假设我们需要编写一个函数,它接收一个参数并原样返回,如果不使用泛型,我们可能会这样写。
function identity(arg: any): any {
return arg;
}
这种写法虽然能够接收任意类型的参数,但返回值类型也变成了any,我们丢失了类型信息。比如传入一个number,TypeScript无法推断返回值也是number类型。
使用泛型则可以完美解决这个问题。
function identity<T>(arg: T): T {
return arg;
}
const num = identity<number>(42); // num的类型是number
const str = identity<string>("hello"); // str的类型是string
代码中,T是一个类型参数,它在函数调用时被具体的类型替换,这样既保持了灵活性,又保留了完整的类型信息。
泛型函数
泛型函数是最常见的泛型用法,我们在函数名后使用尖括号<>定义类型参数,下面是一个有趣但有点复杂的例子。
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]];
}
const result = swap<string, number>(["hello", 42]); // 类型为[number, string]
例子中,我们定义了两个类型参数T和U,函数接收一个元组并交换其中元素的顺序。
在大多数情况下,TypeScript能够根据传入的参数自动推断类型参数,因此我们可以省略显式的类型指定。
const result = swap(["hello", 42]); // TypeScript自动推断类型
此外箭头函数同样支持泛型语法。
const identity = <T>(arg: T): T => {
return arg;
};
泛型接口
接口也可以使用泛型,这在定义通用数据结构时非常有用。
interface Response<T> {
code: number;
message: string;
data: T;
}
interface User {
id: number;
name: string;
}
const userResponse: Response<User> = {
code: 200,
message: "success",
data: {
id: 1,
name: "Tom",
},
};
上面例子中,我们定义了一个通用的响应接口Response<T>,其中data字段的类型由泛型参数T决定,这种模式在处理API响应时非常常见。
泛型接口也可以用于描述函数类型。
interface GenericFunction<T> {
(arg: T): T;
}
const myIdentity: GenericFunction<number> = (arg) => arg;
泛型类
类同样支持泛型,泛型类在实例化时指定具体类型。
class Container<T> {
private value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
setValue(value: T): void {
this.value = value;
}
}
const numberContainer = new Container<number>(42);
const stringContainer = new Container<string>("hello");
console.log(numberContainer.getValue()); // 42
console.log(stringContainer.getValue()); // hello
注意:泛型类的类型参数只能用于实例成员,不能用于静态成员。这是因为类型参数的作用域和生命周期与实例相关,而静态成员属于类本身,与任何具体实例无关,在Java、C#等语言中也是类似的。
class Container<T> {
static defaultValue: T; // 错误:静态成员不能引用类型参数
}
泛型约束
有时我们需要限制泛型参数的类型范围,这时可以使用泛型约束。通过extends关键字,我们可以要求类型参数必须满足某些条件。
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): void {
console.log(arg.length);
}
logLength("hello"); // 正确,字符串有length属性
logLength([1, 2, 3]); // 正确,数组有length属性
logLength({ length: 10 }); // 正确,对象有length属性
logLength(42); // 错误,number类型没有length属性
上面例子中,我们约束类型参数T必须具有length属性,这样在函数内部就可以安全地访问arg.length。
我们还可以使用类型参数作为另一个类型参数的约束。
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person = { name: "Tom", age: 25 };
const name = getProperty(person, "name"); // 正确
const value = getProperty(person, "email"); // 错误,"email"不是person的键
这里K extends keyof T表示K必须是T的键之一,这保证了我们只能访问对象上实际存在的属性。
泛型默认值
类似于函数参数的默认值,泛型参数也可以指定默认类型。
interface Container<T = string> {
value: T;
}
const c1: Container = { value: "hello" }; // T默认为string
const c2: Container<number> = { value: 42 }; // 显式指定T为number
当泛型有多个类型参数时,有默认值的参数必须放在没有默认值的参数后面。
interface KeyValue<K, V = string> {
key: K;
value: V;
}
高级泛型工具类型
TypeScript内置了一些常用的高级泛型工具类型,它们可以帮助我们进行类型转换。
Partial\<T>:将类型T的所有属性变为可选。
interface User {
id: number;
name: string;
email: string;
}
type PartialUser = Partial<User>;
// 等价于 { id?: number; name?: string; email?: string; }
Required\<T>:将类型T的所有属性变为必选。
interface Config {
host?: string;
port?: number;
}
type RequiredConfig = Required<Config>;
// 等价于 { host: string; port: number; }
Readonly\<T>:将类型T的所有属性变为只读。
interface Point {
x: number;
y: number;
}
type ReadonlyPoint = Readonly<Point>;
const p: ReadonlyPoint = { x: 1, y: 2 };
p.x = 3; // 错误,属性是只读的
Pick\<T, K>:从类型T中选取部分属性K。
interface User {
id: number;
name: string;
email: string;
age: number;
}
type UserBasicInfo = Pick<User, "id" | "name">;
// 等价于 { id: number; name: string; }
Omit\<T, K>:从类型T中排除部分属性K。
type UserWithoutEmail = Omit<User, "email">;
// 等价于 { id: number; name: string; age: number; }
Record\<K, T>:创建一个键类型为K、值类型为T的对象类型。
type StringMap = Record<string, number>;
const scores: StringMap = {
math: 90,
english: 85,
};
ReturnType\<T>:获取函数类型T的返回值类型。
function createUser() {
return { id: 1, name: "Tom" };
}
type User = ReturnType<typeof createUser>;
// 等价于 { id: number; name: string; }
这些高级工具类型在实际开发中使用频率很高,熟练掌握它们可以显著提升代码的类型表达能力。