阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 泛型与映射类型结合

泛型与映射类型结合

作者:陈川 阅读数:9576人阅读 分类: TypeScript

泛型与映射类型结合的基本概念

TypeScript中泛型允许创建可重用的组件,而映射类型则能基于现有类型生成新类型。两者结合使用时,可以构建出更灵活、更强大的类型系统。泛型提供了类型参数化的能力,映射类型则通过遍历已有类型的属性来转换类型,这种组合特别适合处理需要动态生成类型的场景。

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

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

type PartialUser = Partial<User>;
// 等价于
// interface PartialUser {
//   id?: number;
//   name?: string;
//   age?: number;
// }

映射类型的核心语法

映射类型的核心语法是[P in K]: T形式,其中K通常是keyof T,表示遍历类型T的所有属性名。结合泛型后,可以创建出能应用于任何类型的通用映射类型。

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

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

interface Product {
  id: string;
  price: number;
  inStock: boolean;
}

type ReadonlyProduct = Readonly<Product>;
// {
//   readonly id: string;
//   readonly price: number;
//   readonly inStock: boolean;
// }

type NullableProduct = Nullable<Product>;
// {
//   id: string | null;
//   price: number | null;
//   inStock: boolean | null;
// }

条件类型与映射类型的结合

当映射类型与条件类型结合时,可以实现更复杂的类型转换。通过extends关键字,可以在映射过程中对属性类型进行条件判断和转换。

type FilterProperties<T, U> = {
  [P in keyof T as T[P] extends U ? P : never]: T[P];
};

interface MixedData {
  name: string;
  age: number;
  isAdmin: boolean;
  createdAt: Date;
}

type StringProperties = FilterProperties<MixedData, string>;
// { name: string }

type NumberProperties = FilterProperties<MixedData, number>;
// { age: number }

递归映射类型

映射类型可以递归应用,用于处理嵌套对象结构。这在处理复杂的数据形状时特别有用,比如深度只读或深度可选类型。

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

interface Company {
  name: string;
  departments: {
    sales: {
      headcount: number;
      budget: number;
    };
    engineering: {
      headcount: number;
      projects: string[];
    };
  };
}

type ReadonlyCompany = DeepReadonly<Company>;
// {
//   readonly name: string;
//   readonly departments: {
//     readonly sales: {
//       readonly headcount: number;
//       readonly budget: number;
//     };
//     readonly engineering: {
//       readonly headcount: number;
//       readonly projects: readonly string[];
//     };
//   };
// }

键重映射的高级用法

TypeScript 4.1引入了键重映射功能,允许在映射过程中修改属性名。这通过as子句实现,为类型转换提供了更多可能性。

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

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

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

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

class Example {
  id: number = 1;
  calculate(): number {
    return this.id * 2;
  }
}

type DataOnly = RemoveMethods<Example>;
// { id: number }

实用工具类型的实现

许多TypeScript内置工具类型实际上就是泛型与映射类型的结合。理解这些实现有助于创建自定义工具类型。

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

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

interface Book {
  title: string;
  author: string;
  pages: number;
  published: boolean;
}

type BookPreview = Pick<Book, 'title' | 'author'>;
// { title: string; author: string }

type PageCounts = Record<'home' | 'about' | 'contact', number>;
// {
//   home: number;
//   about: number;
//   contact: number;
// }

处理联合类型的映射

当映射类型应用于联合类型时,会产生分布式条件类型的效果。这在处理可辨识联合类型时特别有用。

type EventMap = {
  click: { x: number; y: number };
  focus: { target: HTMLElement };
  keydown: { key: string; code: number };
};

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

type AppEventHandlers = EventHandlers<EventMap>;
// {
//   onClick: (event: { x: number; y: number }) => void;
//   onFocus: (event: { target: HTMLElement }) => void;
//   onKeydown: (event: { key: string; code: number }) => void;
// }

类型谓词与映射类型

结合类型谓词,可以在映射类型中实现更精确的类型守卫,这在处理复杂类型系统时能提供更好的类型安全性。

type NonNullablePropertyKeys<T> = {
  [K in keyof T]: null extends T[K] ? never : K;
}[keyof T];

type NonNullableProperties<T> = Pick<T, NonNullablePropertyKeys<T>>;

interface Settings {
  theme: 'light' | 'dark';
  fontSize?: number;
  animations: boolean | null;
}

type RequiredSettings = NonNullableProperties<Settings>;
// { theme: 'light' | 'dark' }

模板字面量类型的映射应用

结合模板字面量类型,映射类型可以创建出更具表达力的API接口,特别是在处理事件处理程序或数据转换时。

type ActionTypes = 'create' | 'update' | 'delete';

type ActionHandlers<T> = {
  [K in T as `handle${Capitalize<string & K>}`]: (payload: any) => void;
};

type AppActions = ActionHandlers<ActionTypes>;
// {
//   handleCreate: (payload: any) => void;
//   handleUpdate: (payload: any) => void;
//   handleDelete: (payload: any) => void;
// }

type ApiEndpoints = 'users' | 'products' | 'orders';

type ApiUrls = {
  [K in ApiEndpoints as `get${Capitalize<K>}`]: string;
} & {
  [K in ApiEndpoints as `post${Capitalize<K>}`]: string;
};

const urls: ApiUrls = {
  getUsers: '/api/users',
  postUsers: '/api/users',
  getProducts: '/api/products',
  postProducts: '/api/products',
  getOrders: '/api/orders',
  postOrders: '/api/orders'
};

性能考虑与优化

虽然映射类型强大,但在处理大型类型或深度嵌套类型时可能会影响编译器性能。了解一些优化技巧很重要。

// 避免过度嵌套
type SimpleMapped<T> = {
  [P in keyof T]: SomeUtilityType<T[P]>;
};

// 使用条件类型提前终止递归
type SafeDeepReadonly<T> = T extends object
  ? {
      readonly [P in keyof T]: SafeDeepReadonly<T[P]>;
    }
  : T;

// 对于大型接口,考虑分解映射
type FirstHalf<T> = Pick<T, /* 选择部分键 */>;
type SecondHalf<T> = Pick<T, /* 选择剩余键 */>;

type MappedFirstHalf<T> = {
  [P in keyof FirstHalf<T>]: /* 转换 */;
};

type MappedSecondHalf<T> = {
  [P in keyof SecondHalf<T>]: /* 转换 */;
};

type FinalType<T> = MappedFirstHalf<T> & MappedSecondHalf<T>;

实际应用场景示例

在实际项目中,泛型与映射类型的结合可以解决许多常见问题,比如API响应处理、表单状态管理等。

// API响应标准化
type ApiResponse<T> =
  | {
      status: 'success';
      data: T;
      timestamp: Date;
    }
  | {
      status: 'error';
      error: string;
      code: number;
      timestamp: Date;
    };

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

type FormState<T> = {
  values: T;
  errors: MakeAllPropertiesOptional<Record<keyof T, string>>;
  touched: MakeAllPropertiesOptional<Record<keyof T, boolean>>;
  isSubmitting: boolean;
};

interface LoginForm {
  email: string;
  password: string;
  rememberMe: boolean;
}

const initialFormState: FormState<LoginForm> = {
  values: {
    email: '',
    password: '',
    rememberMe: false
  },
  errors: {},
  touched: {},
  isSubmitting: false
};

// 动态生成Redux action类型
type ActionMap<M extends { [index: string]: any }> = {
  [Key in keyof M]: M[Key] extends undefined
    ? {
        type: Key;
      }
    : {
        type: Key;
        payload: M[Key];
      };
};

type CounterActions = ActionMap<{
  increment: undefined;
  decrement: undefined;
  setCount: number;
  reset: string;
}>;

// 生成的类型:
// {
//   increment: { type: 'increment' };
//   decrement: { type: 'decrement' };
//   setCount: { type: 'setCount'; payload: number };
//   reset: { type: 'reset'; payload: string };
// }

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

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

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌