晚上好,各位未来的代码大师们!今天咱们来聊聊 TypeScript 里一个相当酷炫的东西——映射类型 (Mapped Types)。这玩意儿,说白了,就是让 TypeScript 拥有了批量处理对象类型的超能力。你想批量把一个类型的属性变成只读?想让所有属性都变成可选?有了映射类型,So easy!
一、啥是映射类型?(别怕,没那么高深)
想象一下,你有一堆饼干,每块饼干都有不同的配料。现在你想给每块饼干都加一层巧克力酱。你会怎么做?一块一块手动涂?太累了吧!映射类型就像一个巧克力酱机器,你把所有饼干(类型)放进去,它自动给每块饼干(属性)都涂上巧克力酱(某种转换)。
用更专业的术语来说,映射类型允许你基于一个已有的类型,创建一个新的类型,新类型的每个属性都经过某种转换。这种转换可以包括:
- 将属性变成只读 (Readonly)
- 将属性变成可选 (Partial)
- 移除属性的只读或可选修饰符
- 改变属性的类型
- 等等…
二、映射类型的基本语法:像写作文一样简单
映射类型的语法长这样:
type NewType<Type> = {
[Property in keyof Type]: TransformType;
};
别被吓到!咱们来拆解一下:
type NewType<Type>
:这定义了一个新的类型NewType
,它是一个泛型类型,接受一个类型参数Type
。就像函数的参数一样,Type
代表你想要转换的原始类型。[Property in keyof Type]
:这是映射类型的核心部分。它使用in
关键字遍历Type
类型的每一个属性。keyof Type
返回Type
类型的所有属性名组成的联合类型。Property
就像一个变量,代表每次遍历到的属性名。TransformType
:这部分定义了属性的转换规则。你可以根据需要对属性的类型进行修改,或者添加只读/可选修饰符。
三、内置的映射类型:TypeScript 官方出品,必属精品
TypeScript 已经内置了一些常用的映射类型,它们能让你事半功倍:
-
Partial<Type>
:把所有属性变成可选interface Person { name: string; age: number; } type OptionalPerson = Partial<Person>; // 等价于: // interface OptionalPerson { // name?: string; // age?: number; // } const person: OptionalPerson = {}; // 合法!
Partial<Person>
把Person
类型的name
和age
属性都变成了可选的。这在处理 API 返回数据或者构建表单时非常有用,因为你可能只需要提供部分属性。 -
Required<Type>
:把所有属性变成必需interface Person { name?: string; age?: number; } type RequiredPerson = Required<Person>; // 等价于: // interface RequiredPerson { // name: string; // age: number; // } const person: RequiredPerson = { name: "Alice", age: 30 }; // 必须提供所有属性
Required<Person>
和Partial<Person>
正好相反,它把Person
类型的name
和age
属性都变成了必需的。 -
Readonly<Type>
:把所有属性变成只读interface Person { name: string; age: number; } type ReadonlyPerson = Readonly<Person>; // 等价于: // interface ReadonlyPerson { // readonly name: string; // readonly age: number; // } const person: ReadonlyPerson = { name: "Bob", age: 40 }; // person.age = 41; // 报错!Cannot assign to 'age' because it is a read-only property.
Readonly<Person>
把Person
类型的name
和age
属性都变成了只读的。这意味着你不能修改这些属性的值。 -
Pick<Type, Keys>
:选择指定属性interface Person { name: string; age: number; address: string; } type NameAndAge = Pick<Person, "name" | "age">; // 等价于: // interface NameAndAge { // name: string; // age: number; // } const person: NameAndAge = { name: "Charlie", age: 50 };
Pick<Person, "name" | "age">
从Person
类型中选择name
和age
属性,创建一个新的类型NameAndAge
。Keys
是一个联合类型,包含你想要选择的属性名。 -
Omit<Type, Keys>
:排除指定属性interface Person { name: string; age: number; address: string; } type PersonWithoutAddress = Omit<Person, "address">; // 等价于: // interface PersonWithoutAddress { // name: string; // age: number; // } const person: PersonWithoutAddress = { name: "David", age: 60 };
Omit<Person, "address">
从Person
类型中排除address
属性,创建一个新的类型PersonWithoutAddress
。Keys
是一个联合类型,包含你想要排除的属性名。
四、自定义映射类型:打造属于你的专属巧克力酱机器
内置的映射类型已经很强大了,但有时候你可能需要更定制化的转换。这时候,就需要你自己动手写映射类型了。
-
将所有属性的类型变成
string
interface Product { id: number; name: string; price: number; } type StringifiedProduct<T> = { }; type ProductStrings = StringifiedProduct<Product>; // 等价于: // interface ProductStrings { // id: string; // name: string; // price: string; // } const product: ProductStrings = { id: "1", name: "Laptop", price: "1200" };
StringifiedProduct<T>
将T
类型的所有属性的类型都变成了string
。 -
添加
get
前缀到所有属性名interface User { firstName: string; lastName: string; } type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]; }; type UserGetters = Getters<User>; // 等价于: // interface UserGetters { // getFirstName: () => string; // getLastName: () => string; // } const userGetters: UserGetters = { getFirstName: () => "Eve", getLastName: () => "Johnson", };
这个例子稍微复杂一点。
Getters<T>
使用了模板字面量类型 (Template Literal Types) 和条件类型 (Conditional Types) 来实现更高级的转换。[K in keyof T as
get${Capitalize<string & K>}`]:这部分是关键。它遍历
T类型的每一个属性,并将属性名修改为
get+ 首字母大写的属性名。
Capitalize<string & K>` 将属性名的第一个字母变成大写。() => T[K]
:这部分定义了属性的类型。它是一个函数,返回值为T[K]
,即原始属性的类型。
-
移除
null
和undefined
类型interface Data { name: string | null; age: number | undefined; address: string | null | undefined; } type NonNullableData<T> = { }; type CleanData = NonNullableData<Data>; // 等价于: // interface CleanData { // name: string; // age: number; // address: string; // }
NonNullableData<T>
使用了内置的NonNullable<T>
类型,它会移除T
类型中的null
和undefined
。
五、映射类型与条件类型:让你的巧克力酱机器更智能
映射类型可以和条件类型结合使用,实现更复杂的转换逻辑。
interface Person {
name: string;
age: number;
address?: string;
}
type StringOrNumber<T> = {
[K in keyof T]: T[K] extends string | number ? T[K] : never;
};
type ValidPerson = StringOrNumber<Person>;
// 等价于:
// interface ValidPerson {
// name: string;
// age: number;
// address?: never;
// }
StringOrNumber<T>
使用了条件类型 T[K] extends string | number ? T[K] : never
。如果属性的类型是 string
或 number
,则保持不变;否则,将其类型设置为 never
。never
类型表示永远不会出现的值,相当于从类型中移除了该属性。在这个例子中,address
属性的类型变成了 never
,这意味着它实际上被从 ValidPerson
类型中移除了。
六、映射类型的应用场景:代码中的瑞士军刀
映射类型在实际开发中有很多应用场景:
- 数据转换: 将 API 返回的数据转换成适合前端使用的格式。
- 表单处理: 根据表单的配置自动生成表单的类型。
- 状态管理: 创建只读的状态对象,防止意外修改。
- 代码生成: 自动生成重复的代码,减少手动编写的工作量。
七、映射类型的注意事项:小心驶得万年船
- 性能: 复杂的映射类型可能会影响编译速度。尽量避免过度使用。
- 可读性: 映射类型可能会使代码难以理解。添加适当的注释,提高代码的可读性。
- 类型推断: TypeScript 的类型推断有时可能会失效。需要手动指定类型。
八、实战演练:一个完整的例子
假设你正在开发一个电商网站,需要处理商品数据。
interface Product {
id: number;
name: string;
description: string;
price: number;
imageUrl: string;
available: boolean;
}
// 1. 创建一个只包含商品名称和价格的类型
type ProductNameAndPrice = Pick<Product, "name" | "price">;
// 2. 创建一个类型,所有属性都是可选的,用于更新商品信息
type UpdateProduct = Partial<Product>;
// 3. 创建一个类型,所有属性都是只读的,用于显示商品详情
type ReadonlyProduct = Readonly<Product>;
// 4. 创建一个类型,将所有属性的类型都变成字符串,用于序列化商品数据
type StringifiedProduct<T> = {
[K in keyof T]: string;
};
type ProductStrings = StringifiedProduct<Product>;
// 5. 创建一个类型,移除 description 属性,用于创建新的商品
type CreateProduct = Omit<Product, "description">;
// 6. 创建一个类型,将所有属性的类型都变成可以为空 (nullable)
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
type NullableProduct = Nullable<Product>;
// 使用这些类型
const productNameAndPrice: ProductNameAndPrice = { name: "Laptop", price: 1200 };
const updateProduct: UpdateProduct = { price: 1300 };
const readonlyProduct: ReadonlyProduct = {
id: 1,
name: "Laptop",
description: "A powerful laptop",
price: 1200,
imageUrl: "laptop.jpg",
available: true,
};
const productStrings: ProductStrings = {
id: "1",
name: "Laptop",
description: "A powerful laptop",
price: "1200",
imageUrl: "laptop.jpg",
available: "true",
};
const createProduct: CreateProduct = {
id: 2,
name: "Mouse",
price: 25,
imageUrl: "mouse.jpg",
available: true,
};
const nullableProduct: NullableProduct = {
id: 1,
name: "Laptop",
description: "A powerful laptop",
price: 1200,
imageUrl: "laptop.jpg",
available: true
};
// 类型别名映射属性
type MapType<T> = {
[K in keyof T]: T[K] extends number ? string : T[K];
}
type TestMap = MapType<{id:number,name:string}> //type TestMap = { id: string; name: string; }
// 基于已存在的类型,我们可以创建一个只读的类型
type ReadOnlyType<T> = {
readonly [P in keyof T]: T[P]
}
// 基于已存在的类型,我们可以创建一个可选的类型
type PartialType<T> = {
[P in keyof T]?: T[P]
}
// 基于已存在的类型,我们可以创建一个pick类型
type PickType<T, K extends keyof T> = {
[P in K]: T[P]
}
// 基于已存在的类型,我们可以创建一个record类型
type RecordType<K extends string | number | symbol, T> = {
[P in K]: T
}
九、总结:掌握映射类型,成为 TypeScript 大师
映射类型是 TypeScript 中一个非常强大的特性,它可以让你轻松地转换对象类型,提高代码的灵活性和可维护性。虽然它的语法可能有点复杂,但只要你理解了它的基本原理,就能灵活运用它来解决各种实际问题。
记住,熟能生巧!多写代码,多练习,你就能掌握映射类型,成为真正的 TypeScript 大师!
今天的讲座就到这里。希望大家有所收获!下次再见!