MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript 映射类型的实际案例分享

2024-05-291.7k 阅读

映射类型基础回顾

在深入实际案例之前,我们先来简要回顾一下TypeScript映射类型的基础知识。映射类型允许我们以一种类型安全且便捷的方式,基于已有的类型创建新类型。其基本语法是通过in关键字迭代类型的属性,并对每个属性应用特定的变换。

例如,假设有一个简单的类型User

type User = {
  name: string;
  age: number;
  email: string;
};

我们可以创建一个只读版本的User类型,使用映射类型:

type ReadonlyUser = {
  readonly [P in keyof User]: User[P];
};

这里,keyof User获取User类型的所有属性名,P是迭代变量,[P in keyof User]表示对User的每个属性进行迭代。readonly关键字使得新类型的属性变为只读。

案例一:属性转换 - 从普通对象到响应式对象

在前端开发框架如Vue.js中,常常需要将普通的JavaScript对象转换为响应式对象,以便在数据变化时自动更新视图。假设我们有一个表示用户信息的普通对象类型UserInfo

type UserInfo = {
  name: string;
  age: number;
  address: string;
};

我们可以创建一个映射类型来将这个普通对象类型转换为一个响应式对象类型。在Vue.js中,响应式对象通常通过reactive函数创建,并且属性会被代理以实现响应式。

首先,定义一个辅助函数来模拟Vue的reactive行为(实际在Vue中是通过@vue/reactivity库实现的,这里为了示例简化):

function reactive<T extends object>(obj: T): T {
  return new Proxy(obj, {
    get(target, prop) {
      return target[prop];
    },
    set(target, prop, value) {
      target[prop] = value;
      // 这里可以添加实际的视图更新逻辑,例如触发依赖更新
      return true;
    }
  });
}

然后,使用映射类型创建响应式对象类型:

type ReactiveUserInfo = {
  [P in keyof UserInfo]: UserInfo[P];
};
function createReactiveUserInfo(userInfo: UserInfo): ReactiveUserInfo {
  return reactive(userInfo) as ReactiveUserInfo;
}

这样,我们就可以将普通的UserInfo对象转换为响应式的ReactiveUserInfo对象。例如:

const user: UserInfo = {
  name: 'John',
  age: 30,
  address: '123 Main St'
};
const reactiveUser = createReactiveUserInfo(user);
console.log(reactiveUser.name); // 输出 'John'
reactiveUser.age = 31; // 这里可以触发潜在的视图更新

在这个案例中,映射类型帮助我们确保了转换后的响应式对象与原始对象的类型一致性,同时也为我们提供了一种类型安全的方式来处理对象的转换。

案例二:表单数据验证与类型映射

在前端开发中,表单数据验证是一个常见的需求。假设我们有一个注册表单,其数据类型如下:

type RegistrationForm = {
  username: string;
  password: string;
  email: string;
  age: number;
};

我们想要创建一个验证函数,该函数不仅验证数据的格式,还返回验证后的类型。例如,username必须是长度在3到20之间的字符串,password必须是长度至少6的字符串,email必须是有效的邮箱格式,age必须是大于18的数字。

首先,定义一些验证函数:

function validateUsername(username: string): boolean {
  return username.length >= 3 && username.length <= 20;
}
function validatePassword(password: string): boolean {
  return password.length >= 6;
}
function validateEmail(email: string): boolean {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}
function validateAge(age: number): boolean {
  return age > 18;
}

然后,使用映射类型创建验证后的表单数据类型:

type ValidatedRegistrationForm = {
  [P in keyof RegistrationForm]: P extends 'username'
  ? validateUsername extends (arg: RegistrationForm[P]) => boolean
    ? RegistrationForm[P]
    : never
  : P extends 'password'
  ? validatePassword extends (arg: RegistrationForm[P]) => boolean
    ? RegistrationForm[P]
    : never
  : P extends 'email'
  ? validateEmail extends (arg: RegistrationForm[P]) => boolean
    ? RegistrationForm[P]
    : never
  : P extends 'age'
  ? validateAge extends (arg: RegistrationForm[P]) => boolean
    ? RegistrationForm[P]
    : never
  : never;
};

这里的映射类型使用了条件类型,根据不同的属性名,判断对应的验证函数是否能验证该属性类型。如果可以验证,则保留该属性类型,否则使用never类型。

最后,实现验证函数:

function validateRegistrationForm(form: RegistrationForm): ValidatedRegistrationForm | null {
  const { username, password, email, age } = form;
  if (!validateUsername(username) ||!validatePassword(password) ||!validateEmail(email) ||!validateAge(age)) {
    return null;
  }
  return form as ValidatedRegistrationForm;
}

这样,我们可以在使用表单数据之前进行类型安全的验证:

const formData: RegistrationForm = {
  username: 'user123',
  password: 'pass123456',
  email: 'user@example.com',
  age: 20
};
const validatedForm = validateRegistrationForm(formData);
if (validatedForm) {
  console.log('Form is valid:', validatedForm);
} else {
  console.log('Form is invalid');
}

通过映射类型和条件类型的结合,我们实现了表单数据的类型安全验证,确保只有符合验证规则的数据才能通过类型检查。

案例三:API响应数据转换与类型适配

在前端开发中,与后端API进行交互是常见的任务。后端返回的数据格式可能并不总是与前端期望的类型完全匹配,这时就需要进行数据转换和类型适配。

假设后端提供了一个获取用户列表的API,其响应数据格式如下:

type ApiUser = {
  id: number;
  name: string;
  age: number;
  created_at: string;
};

而前端期望的用户列表类型如下:

type FrontendUser = {
  userId: number;
  fullName: string;
  userAge: number;
  createdAt: Date;
};

我们可以使用映射类型来创建一个转换函数,并确保转换后的数据类型正确。

首先,定义转换函数:

function convertApiUserToFrontendUser(apiUser: ApiUser): FrontendUser {
  return {
    userId: apiUser.id,
    fullName: apiUser.name,
    userAge: apiUser.age,
    createdAt: new Date(apiUser.created_at)
  };
}

然后,使用映射类型来创建批量转换函数,并确保返回类型正确:

type ApiUserList = ApiUser[];
type FrontendUserList = FrontendUser[];
function convertApiUserListToFrontendUserList(apiUserList: ApiUserList): FrontendUserList {
  return apiUserList.map(convertApiUserToFrontendUser);
}

这里,虽然没有直接在映射类型中进行复杂的属性变换,但通过函数结合映射类型,我们确保了数据转换的类型安全性。例如:

const apiUsers: ApiUserList = [
  { id: 1, name: 'Alice', age: 25, created_at: '2023 - 01 - 01T00:00:00Z' },
  { id: 2, name: 'Bob', age: 30, created_at: '2023 - 02 - 01T00:00:00Z' }
];
const frontendUsers = convertApiUserListToFrontendUserList(apiUsers);
console.log(frontendUsers);

在这个案例中,映射类型帮助我们在整体数据结构层面(数组类型)确保了数据转换的类型安全,使得前端可以安全地使用从API获取并转换后的数据。

案例四:状态管理中的类型转换

在前端状态管理库如Redux中,我们经常需要定义各种状态类型、动作类型以及它们之间的映射关系。假设我们有一个简单的计数器应用,其状态类型如下:

type CounterState = {
  value: number;
  isLoading: boolean;
};

我们定义一些动作类型,例如增加计数器的值、减少计数器的值以及设置加载状态:

type IncrementAction = { type: 'INCREMENT' };
type DecrementAction = { type: 'DECREMENT' };
type SetLoadingAction = { type: 'SET_LOADING', payload: boolean };
type CounterAction = IncrementAction | DecrementAction | SetLoadingAction;

现在,我们可以使用映射类型来创建一个函数,该函数根据不同的动作类型更新状态。首先,定义状态更新函数:

function counterReducer(state: CounterState, action: CounterAction): CounterState {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, value: state.value + 1 };
    case 'DECREMENT':
      return { ...state, value: state.value - 1 };
    case 'SET_LOADING':
      return { ...state, isLoading: action.payload };
    default:
      return state;
  }
}

然后,我们可以使用映射类型来创建一个类型安全的动作分发函数。假设我们有一个dispatch函数,它接受一个动作对象并调用counterReducer来更新状态:

type ActionDispatchers = {
  [P in CounterAction['type']]: (payload?: any) => CounterAction;
};
const actionDispatchers: ActionDispatchers = {
  INCREMENT: () => ({ type: 'INCREMENT' }),
  DECREMENT: () => ({ type: 'DECREMENT' }),
  SET_LOADING: (payload) => ({ type: 'SET_LOADING', payload })
};

这里的映射类型ActionDispatchers基于CounterActiontype属性创建了一个对象类型,其中每个属性对应一个动作分发函数。这样,我们在使用动作分发函数时可以获得更好的类型提示。例如:

let counterState: CounterState = { value: 0, isLoading: false };
counterState = counterReducer(counterState, actionDispatchers.INCREMENT());
counterState = counterReducer(counterState, actionDispatchers.SET_LOADING(true));
console.log(counterState);

通过映射类型,我们在状态管理中实现了动作分发的类型安全,使得代码更加健壮和易于维护。

案例五:组件属性类型转换

在前端组件库开发中,经常需要对组件的属性进行类型转换。假设我们有一个Button组件,其属性类型如下:

type ButtonProps = {
  label: string;
  disabled: boolean;
  size: 'small' | 'medium' | 'large';
};

现在,我们想要创建一个新的LinkButton组件,它继承了Button组件的部分属性,但有一些属性名的变化。例如,label变为textdisabled保持不变,size变为linkSize

我们可以使用映射类型来创建LinkButton的属性类型:

type LinkButtonProps = {
  [P in keyof ButtonProps as P extends 'label' ? 'text' : P extends 'size' ? 'linkSize' : P]: ButtonProps[P];
};

这里使用了映射类型的as关键字来重命名属性。as关键字后面的条件表达式根据原属性名决定新的属性名。

然后,我们可以实现LinkButton组件:

function LinkButton({ text, disabled, linkSize }: LinkButtonProps) {
  // 组件逻辑,例如渲染一个带有链接样式的按钮
  return <a disabled={disabled} className={`button ${linkSize}`}>{text}</a>;
}

这样,我们可以安全地使用LinkButton组件,并确保其属性类型与预期一致:

const linkButtonProps: LinkButtonProps = {
  text: 'Click me',
  disabled: false,
  linkSize: 'medium'
};
LinkButton(linkButtonProps);

通过映射类型,我们在组件属性类型转换过程中保持了类型的一致性和安全性,使得组件库的开发和使用更加便捷和可靠。

案例六:国际化文本类型映射

在多语言应用开发中,需要管理不同语言的文本。假设我们有一个简单的应用,其文本类型如下:

type AppTexts = {
  welcomeMessage: string;
  goodbyeMessage: string;
  errorMessage: string;
};

我们可以使用映射类型来创建不同语言版本的文本类型。例如,英文版本和中文版本:

type EnglishTexts = {
  [P in keyof AppTexts]: string;
};
type ChineseTexts = {
  [P in keyof AppTexts]: string;
};
const englishTexts: EnglishTexts = {
  welcomeMessage: 'Welcome!',
  goodbyeMessage: 'Goodbye!',
  errorMessage: 'An error occurred'
};
const chineseTexts: ChineseTexts = {
  welcomeMessage: '欢迎!',
  goodbyeMessage: '再见!',
  errorMessage: '发生错误'
};

然后,我们可以创建一个函数来根据当前语言获取对应的文本:

type Language = 'en' | 'zh';
function getTexts(language: Language): AppTexts {
  return language === 'en'? englishTexts : chineseTexts;
}

这里通过映射类型确保了不同语言文本类型的一致性,同时使得获取文本的函数在类型上更加安全。例如:

const currentLanguage: Language = 'zh';
const currentTexts = getTexts(currentLanguage);
console.log(currentTexts.welcomeMessage);

在国际化应用开发中,映射类型有助于管理和维护不同语言版本的文本类型,提高代码的可维护性和类型安全性。

案例七:动态表单字段类型映射

在一些复杂的前端应用中,可能需要创建动态表单,其字段类型和数量根据业务需求动态变化。假设我们有一个基本的表单字段类型:

type FormField = {
  id: string;
  label: string;
  type: 'text' | 'number' | 'select';
  value: string | number | string[];
};

现在,我们想要根据一个配置对象动态创建表单字段。配置对象可能如下:

type FormFieldConfig = {
  [fieldId: string]: {
    label: string;
    type: 'text' | 'number' | 'select';
    defaultValue: string | number | string[];
  };
};

我们可以使用映射类型来创建实际的表单字段类型:

type GeneratedFormFields = {
  [P in keyof FormFieldConfig]: FormField & {
    value: FormFieldConfig[P]['defaultValue'];
  };
};
function generateFormFields(config: FormFieldConfig): GeneratedFormFields {
  const fields: any = {};
  for (const fieldId in config) {
    if (config.hasOwnProperty(fieldId)) {
      const { label, type, defaultValue } = config[fieldId];
      fields[fieldId] = {
        id: fieldId,
        label,
        type,
        value: defaultValue
      };
    }
  }
  return fields as GeneratedFormFields;
}

这样,我们可以根据配置对象安全地生成表单字段:

const formConfig: FormFieldConfig = {
  username: {
    label: 'Username',
    type: 'text',
    defaultValue: ''
  },
  age: {
    label: 'Age',
    type: 'number',
    defaultValue: 0
  }
};
const formFields = generateFormFields(formConfig);
console.log(formFields.username.value);

通过映射类型,我们在动态表单生成过程中实现了类型安全,确保生成的表单字段与配置对象的类型一致。

案例八:数据筛选与类型过滤

在处理大量数据时,常常需要根据某些条件进行数据筛选,并且确保筛选后的数据类型仍然正确。假设我们有一个表示书籍的类型:

type Book = {
  id: number;
  title: string;
  author: string;
  year: number;
  genre: 'fiction' | 'non - fiction' | 'biography';
};

我们想要创建一个函数,根据书籍的类型筛选书籍,并返回筛选后的书籍列表。首先,定义筛选函数:

function filterBooksByGenre(books: Book[], genre: Book['genre']): Book[] {
  return books.filter(book => book.genre === genre);
}

现在,我们可以使用映射类型来创建一个类型,该类型表示特定类型书籍的列表。例如,只包含小说类型书籍的列表类型:

type FictionBooksList = {
  [P in keyof Book[] as Book[P] extends { genre: 'fiction' }? P : never]: Book[P];
};
function getFictionBooks(books: Book[]): FictionBooksList {
  return filterBooksByGenre(books, 'fiction') as FictionBooksList;
}

这里通过映射类型结合条件类型,根据书籍的genre属性过滤出小说类型的书籍,并确保返回类型正确。例如:

const allBooks: Book[] = [
  { id: 1, title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', year: 1925, genre: 'fiction' },
  { id: 2, title: 'Sapiens: A Brief History of Humankind', author: 'Yuval Noah Harari', year: 2014, genre: 'non - fiction' },
  { id: 3, title: 'Steve Jobs', author: 'Walter Isaacson', year: 2011, genre: 'biography' },
  { id: 4, title: 'To Kill a Mockingbird', author: 'Harper Lee', year: 1960, genre: 'fiction' }
];
const fictionBooks = getFictionBooks(allBooks);
console.log(fictionBooks);

通过映射类型和条件类型的结合,我们在数据筛选过程中保持了类型的精确性和安全性,使得筛选后的数据可以安全地使用。

案例九:权限控制与类型映射

在企业级前端应用开发中,权限控制是非常重要的。假设我们有一个用户角色类型和一些操作类型:

type UserRole = 'admin' | 'editor' | 'viewer';
type Action = 'create' | 'read' | 'update' | 'delete';

我们可以使用映射类型来创建一个权限映射,即不同角色可以执行的操作。例如:

type PermissionMap = {
  [P in UserRole]: Action[];
};
const permissionMap: PermissionMap = {
  admin: ['create','read', 'update', 'delete'],
  editor: ['read', 'update'],
  viewer: ['read']
};

这样,我们可以很方便地根据用户角色获取其权限。例如,判断一个用户是否有权限执行某个操作:

function hasPermission(role: UserRole, action: Action): boolean {
  return permissionMap[role].includes(action);
}
const currentRole: UserRole = 'editor';
const actionToCheck: Action = 'update';
if (hasPermission(currentRole, actionToCheck)) {
  console.log('User has permission');
} else {
  console.log('User does not have permission');
}

通过映射类型,我们在权限控制中实现了清晰的类型映射,使得权限管理代码更加可读和易于维护。

案例十:树形结构数据类型转换

在前端开发中,树形结构数据很常见,例如菜单、组织结构等。假设我们有一个简单的树形结构类型表示菜单:

type MenuItem = {
  id: number;
  label: string;
  children?: MenuItem[];
};

现在,我们想要将这个菜单结构转换为扁平结构,其中每个菜单项包含其完整路径。我们可以使用映射类型来辅助这个转换过程。

首先,定义一个辅助函数来生成路径:

function generatePath(item: MenuItem, parentPath: string = ''): string {
  const newPath = parentPath? `${parentPath}/${item.label}` : item.label;
  if (item.children) {
    return item.children.map(child => generatePath(child, newPath)).join(', ');
  }
  return newPath;
}

然后,使用映射类型创建扁平菜单类型:

type FlatMenuItem = {
  id: number;
  label: string;
  path: string;
};
type FlatMenu = {
  [P in keyof MenuItem[]]: FlatMenuItem;
};
function flattenMenu(menu: MenuItem[]): FlatMenu {
  const flatMenu: FlatMenu = [];
  function flatten(item: MenuItem, parentPath: string = '') {
    const newItem: FlatMenuItem = {
      id: item.id,
      label: item.label,
      path: generatePath(item, parentPath)
    };
    flatMenu.push(newItem);
    if (item.children) {
      item.children.forEach(child => flatten(child, newItem.path));
    }
  }
  menu.forEach(item => flatten(item));
  return flatMenu;
}

这样,我们可以将树形菜单转换为扁平菜单,并确保类型正确:

const menu: MenuItem[] = [
  { id: 1, label: 'Home' },
  { id: 2, label: 'Products', children: [
    { id: 3, label: 'Product 1' },
    { id: 4, label: 'Product 2' }
  ] },
  { id: 5, label: 'About' }
];
const flatMenu = flattenMenu(menu);
console.log(flatMenu);

通过映射类型,我们在树形结构到扁平结构的数据转换过程中保持了类型安全,使得转换后的数据可以安全地用于后续操作。