JavaScript内核与高级编程之:`TypeScript` 的 `Mapped Types`:如何使用映射类型转换对象类型。

晚上好,各位未来的代码大师们!今天咱们来聊聊 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 已经内置了一些常用的映射类型,它们能让你事半功倍:

  1. Partial<Type>:把所有属性变成可选

    interface Person {
      name: string;
      age: number;
    }
    
    type OptionalPerson = Partial<Person>;
    
    // 等价于:
    // interface OptionalPerson {
    //   name?: string;
    //   age?: number;
    // }
    
    const person: OptionalPerson = {}; // 合法!

    Partial<Person>Person 类型的 nameage 属性都变成了可选的。这在处理 API 返回数据或者构建表单时非常有用,因为你可能只需要提供部分属性。

  2. 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 类型的 nameage 属性都变成了必需的。

  3. 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 类型的 nameage 属性都变成了只读的。这意味着你不能修改这些属性的值。

  4. 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 类型中选择 nameage 属性,创建一个新的类型 NameAndAgeKeys 是一个联合类型,包含你想要选择的属性名。

  5. 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 属性,创建一个新的类型 PersonWithoutAddressKeys 是一个联合类型,包含你想要排除的属性名。

四、自定义映射类型:打造属于你的专属巧克力酱机器

内置的映射类型已经很强大了,但有时候你可能需要更定制化的转换。这时候,就需要你自己动手写映射类型了。

  1. 将所有属性的类型变成 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

  2. 添加 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],即原始属性的类型。
  3. 移除 nullundefined 类型

    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 类型中的 nullundefined

五、映射类型与条件类型:让你的巧克力酱机器更智能

映射类型可以和条件类型结合使用,实现更复杂的转换逻辑。

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。如果属性的类型是 stringnumber,则保持不变;否则,将其类型设置为 nevernever 类型表示永远不会出现的值,相当于从类型中移除了该属性。在这个例子中,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 大师!

今天的讲座就到这里。希望大家有所收获!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注