阿里云主机折上折
  • 微信号
Current Site:Index > Basic concepts of mapping types

Basic concepts of mapping types

Author:Chuan Chen 阅读数:14317人阅读 分类: TypeScript

Basics of Mapped Types

TypeScript's mapped types are a powerful tool for creating new types based on existing ones. They allow us to generate new types by iterating over the properties of an existing type and applying transformation rules, which is particularly useful when working with complex types.

Basic Syntax

The core syntax of mapped types uses the in keyword to iterate over a union type:

type MappedType<T> = {
  [P in keyof T]: T[P];
};

This simplest mapped type essentially creates a new type identical to the original type T. keyof T retrieves a union type of all property names of type T, and P in iterates over each property in this union type.

Common Built-in Mapped Types

TypeScript includes several commonly used built-in mapped types:

// Makes all properties optional
type Partial<T> = {
  [P in keyof T]?: T[P];
};

// Makes all properties required
type Required<T> = {
  [P in keyof T]-?: T[P];
};

// Makes all properties read-only
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

Property Modifier Operations

Mapped types can manipulate property modifiers (? and readonly):

// Removes the optional modifier
type Concrete<T> = {
  [P in keyof T]-?: T[P];
};

// Adds the readonly modifier
type Locked<T> = {
  +readonly [P in keyof T]: T[P];
};

// Removes the readonly modifier
type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
};

Key Remapping

TypeScript 4.1 introduced key remapping, allowing property names to be modified during the mapping process:

type Getters<T> = {
  [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};

interface Person {
  name: string;
  age: number;
}

type PersonGetters = Getters<Person>;
// Equivalent to:
// {
//   getName: () => string;
//   getAge: () => number;
// }

Combining Conditional Types with Mapped Types

Mapped types can be combined with conditional types to achieve more complex type transformations:

type NullableProperties<T> = {
  [P in keyof T]: T[P] | null;
};

interface User {
  id: number;
  name: string;
  email: string;
}

type NullableUser = NullableProperties<User>;
// Equivalent to:
// {
//   id: number | null;
//   name: string | null;
//   email: string | null;
// }

Filtering Properties

Conditional types can be used to filter out certain properties:

type OnlyFunctions<T> = {
  [P in keyof T as T[P] extends Function ? P : never]: T[P];
};

interface Mixed {
  id: number;
  name: string;
  update: (data: Partial<Mixed>) => void;
  delete: () => Promise<void>;
}

type Functions = OnlyFunctions<Mixed>;
// Equivalent to:
// {
//   update: (data: Partial<Mixed>) => void;
//   delete: () => Promise<void>;
// }

Practical Application Examples

Form Type Generation

interface User {
  username: string;
  password: string;
  age: number;
}

type FormField<T> = {
  value: T;
  isValid: boolean;
  errorMessage?: string;
};

type UserForm = {
  [P in keyof User]: FormField<User[P]>;
};

// Equivalent to:
// {
//   username: FormField<string>;
//   password: FormField<string>;
//   age: FormField<number>;
// }

API Response Wrapping

type ApiResponse<T> = {
  data: T;
  status: number;
  timestamp: Date;
};

type Entity = {
  id: string;
  createdAt: Date;
};

type EntityResponse<T extends Entity> = ApiResponse<{
  [P in keyof T]: T[P];
}>;

interface Product extends Entity {
  name: string;
  price: number;
}

type ProductResponse = EntityResponse<Product>;
// Equivalent to:
// ApiResponse<{
//   id: string;
//   createdAt: Date;
//   name: string;
//   price: number;
// }>

Advanced Patterns

Recursive Mapping

Mapped types can be recursively applied to nested objects:

type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

interface Company {
  name: string;
  departments: {
    name: string;
    employees: number;
  }[];
}

type ReadonlyCompany = DeepReadonly<Company>;
// Equivalent to:
// {
//   readonly name: string;
//   readonly departments: readonly {
//     readonly name: string;
//     readonly employees: number;
//   }[];
// }

Union Type Mapping

Mapped types can also be used with union types:

type EventMap = {
  click: MouseEvent;
  focus: FocusEvent;
  keydown: KeyboardEvent;
};

type Handlers<T> = {
  [P in keyof T as `on${Capitalize<string & P>}`]: (event: T[P]) => void;
};

type EventHandlers = Handlers<EventMap>;
// Equivalent to:
// {
//   onClick: (event: MouseEvent) => void;
//   onFocus: (event: FocusEvent) => void;
//   onKeydown: (event: KeyboardEvent) => void;
// }

Performance Considerations

While mapped types are very powerful, performance issues should be considered when dealing with large or deeply nested types. The TypeScript compiler needs to expand these types, and complex mapped types may lead to slower compilation or increased memory usage.

// Avoid overly nested mapped types
type OverlyComplex<T> = {
  [P in keyof T]: {
    [Q in keyof T[P]]: {
      [R in keyof T[P][Q]]: T[P][Q][R];
    };
  };
};

Combining with Template Literal Types

Template literal types introduced in TypeScript 4.1 can be perfectly combined with mapped types:

type WithChanges<T> = {
  [P in keyof T as `${string & P}Changed`]: (newValue: T[P]) => void;
};

interface Settings {
  theme: string;
  fontSize: number;
}

type SettingsChanges = WithChanges<Settings>;
// Equivalent to:
// {
//   themeChanged: (newValue: string) => void;
//   fontSizeChanged: (newValue: number) => void;
// }

Utility Type Implementations

Many utility types are implemented based on mapped types:

// Select specific properties
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

// Exclude specific properties
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

// Record type
type Record<K extends keyof any, T> = {
  [P in K]: T;
};

Type Predicates and Mapped Types

Mapped types can be combined with type predicates to create more precise type guards:

type TypeGuardMap<T> = {
  [P in keyof T]: (value: unknown) => value is T[P];
};

interface Config {
  port: number;
  hostname: string;
  ssl: boolean;
}

const configGuards: TypeGuardMap<Config> = {
  port: (value): value is number => typeof value === 'number',
  hostname: (value): value is string => typeof value === 'string',
  ssl: (value): value is boolean => typeof value === 'boolean',
};

Mapped Types and Generic Constraints

Mapped types can be used with generic constraints to create safer APIs:

type NonMethodKeys<T> = {
  [P in keyof T]: T[P] extends Function ? never : P;
}[keyof T];

type DataProperties<T> = Pick<T, NonMethodKeys<T>>;

class User {
  id: string;
  name: string;
  save() {
    // Save logic
  }
}

type UserData = DataProperties<User>;
// Equivalent to:
// {
//   id: string;
//   name: string;
// }

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.