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

TypeScript Partial 和 Readonly 映射类型的妙用

2023-05-023.5k 阅读

1. 理解 TypeScript 映射类型

在深入探讨 PartialReadonly 之前,我们先来理解一下 TypeScript 中的映射类型。映射类型是一种基于现有类型创建新类型的方式,它允许我们对类型的属性进行转换、修改或者限制。

假设我们有一个简单的类型 User

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

映射类型的基本语法是通过 in 关键字遍历已有类型的属性,并对每个属性进行操作。例如,我们可以创建一个新类型,将 User 类型的所有属性变为可选的:

type OptionalUser = {
  [P in keyof User]?: User[P];
};

这里,keyof User 获取 User 类型的所有属性名,P 是遍历过程中的每个属性名,User[P] 获取属性 P 的类型。? 使得每个属性变为可选。

2. Partial 映射类型

2.1 Partial 的定义

Partial 是 TypeScript 内置的映射类型,它将一个类型的所有属性变为可选的。其定义如下:

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

这个定义和我们上面手动创建的 OptionalUser 类型的逻辑是一样的。

2.2 使用场景

数据更新操作:在前端开发中,我们经常需要更新数据。比如我们有一个 API 接口用于更新用户信息,但是我们可能只需要传递部分需要更新的字段,而不是整个用户对象。

const user: User = {
  name: 'John',
  age: 30,
  email: 'john@example.com'
};

function updateUser(user: User, updates: Partial<User>) {
  return {...user, ...updates };
}

const newUser = updateUser(user, { age: 31 });
console.log(newUser); 

在这个例子中,updateUser 函数接受一个完整的 User 对象和一个 Partial<User> 对象。Partial<User> 类型允许我们只传递需要更新的属性,这样代码更加灵活和安全。

初始化对象:当我们初始化一个对象,但某些属性可能在稍后才会被赋值时,Partial 非常有用。

let newUser2: Partial<User> = {};
newUser2.name = 'Jane';
// 这里 newUser2.age 和 newUser2.email 还未赋值,在后续代码中可以根据需要赋值

2.3 与其他类型结合使用

Partial 可以与其他类型结合使用,进一步增强类型的表达能力。例如,我们可以创建一个函数,接受一个 Partial<User> 类型的参数,并且要求 name 属性必须存在。

type NameRequiredUser = {
  name: string;
} & Partial<Omit<User, 'name'>>;

function createUser(user: NameRequiredUser) {
  // 处理创建用户逻辑
}

createUser({ name: 'Bob', age: 25 }); 

这里,Omit<User, 'name'> 移除了 User 类型中的 name 属性,然后通过 & 运算符与 { name: string; } 结合,使得 name 属性必须存在,而其他属性是可选的。

3. Readonly 映射类型

3.1 Readonly 的定义

Readonly 也是 TypeScript 内置的映射类型,它将一个类型的所有属性变为只读的。其定义如下:

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

通过在属性定义前加上 readonly 关键字,使得属性只能在对象初始化时赋值,之后不能再修改。

3.2 使用场景

常量数据:当我们有一些数据在应用程序的生命周期内不应该被修改时,Readonly 非常有用。例如,我们有一个配置对象:

const config: Readonly<{
  apiUrl: string;
  appName: string;
}> = {
  apiUrl: 'https://example.com/api',
  appName: 'MyApp'
};

// 以下操作会报错,因为 config 是只读的
// config.apiUrl = 'https://newexample.com/api'; 

这样可以确保配置数据不会被意外修改,提高代码的稳定性和可维护性。

不可变数据结构:在一些函数式编程风格的代码中,我们希望数据是不可变的。例如,我们有一个函数接受一个只读的用户对象:

function printUser(user: Readonly<User>) {
  console.log(`Name: ${user.name}, Age: ${user.age}, Email: ${user.email}`);
}

const readonlyUser: Readonly<User> = {
  name: 'Alice',
  age: 28,
  email: 'alice@example.com'
};

printUser(readonlyUser); 

通过使用 Readonly<User> 类型,我们可以确保在 printUser 函数内部不会意外修改用户对象的数据。

3.3 与其他类型结合使用

Readonly 也可以与其他类型结合使用。比如,我们可以创建一个只读且部分属性可选的类型。

type ReadonlyPartialUser = Readonly<Partial<User>>;

let readonlyPartialUser: ReadonlyPartialUser = { name: 'Eve' };
// 以下操作会报错,因为 readonlyPartialUser 是只读的
// readonlyPartialUser.age = 32; 

这样的类型在某些场景下非常有用,例如我们从服务器获取部分用户信息,并且希望确保这些信息在本地不会被修改。

4. Partial 和 Readonly 的深层次原理

4.1 类型推导与约束

PartialReadonly 的实现中,关键在于 in 关键字的使用以及对属性的操作。in keyof T 这一步是对类型 T 的属性名进行遍历。在 Partial 中,通过添加 ? 来改变属性的可选性,而在 Readonly 中,通过添加 readonly 关键字来改变属性的可写性。

这种类型推导过程其实是 TypeScript 类型系统对属性进行重新定义的过程。它基于已有的类型结构,根据我们设定的规则(可选性或只读性)创建一个新的类型。同时,TypeScript 的类型检查机制会严格按照新创建的类型进行约束,确保代码的类型安全性。

4.2 与 JavaScript 对象行为的关系

在 JavaScript 中,对象的属性默认是可写的,除非我们使用 Object.defineProperty 等方法来设置属性的 writable 特性为 false 使其变为只读。TypeScript 的 Readonly 类型实际上是在类型层面模拟了这种只读属性的行为。

Partial 类型则是模拟了 JavaScript 中对象属性可选的情况。在 JavaScript 中,我们可以创建一个对象并选择性地添加属性。TypeScript 的 Partial 类型使得这种行为在类型层面得到了明确的表达和约束。

4.3 泛型与类型参数的作用

PartialReadonly 都是泛型类型,即它们接受一个类型参数 T。泛型的使用使得这两个映射类型具有很高的通用性,可以应用于任何类型。通过传递不同的类型参数,我们可以根据具体的需求创建出不同的部分可选或只读类型。

例如,我们不仅可以应用在 User 类型上,还可以应用在自定义的其他复杂类型上:

type ComplexType = {
  data: {
    subData1: string;
    subData2: number;
  };
  flag: boolean;
};

const readonlyComplex: Readonly<ComplexType> = {
  data: { subData1: 'value1', subData2: 10 },
  flag: true
};

// 以下操作会报错,因为 readonlyComplex 是只读的
// readonlyComplex.flag = false; 

这种通用性使得 PartialReadonly 在不同的项目场景中都能发挥重要作用。

5. 在 React 开发中的应用

5.1 React 组件 Props

在 React 开发中,PartialReadonly 对于定义组件的 props 非常有用。

使用 Partial 定义可选 props:当我们有一个组件,部分 props 是可选的,我们可以使用 Partial。例如,我们有一个 Button 组件:

import React from'react';

type ButtonProps = {
  label: string;
  onClick: () => void;
  disabled?: boolean;
};

const Button: React.FC<Partial<ButtonProps>> = ({ label, onClick, disabled = false }) => {
  return (
    <button disabled={disabled} onClick={onClick}>
      {label}
    </button>
  );
};

export default Button;

这里,Partial<ButtonProps> 使得 disabled 属性是可选的,并且在组件内部我们可以设置默认值。

使用 Readonly 确保 props 不可变:React 强调单向数据流,组件的 props 不应该在组件内部被修改。Readonly 可以帮助我们在类型层面确保这一点。

import React from'react';

type UserProfileProps = Readonly<{
  user: {
    name: string;
    age: number;
  };
}>;

const UserProfile: React.FC<UserProfileProps> = ({ user }) => {
  // 以下操作会报错,因为 user 是只读的
  // user.age = 35; 
  return (
    <div>
      <p>Name: {user.name}</p>
      <p>Age: {user.age}</p>
    </div>
  );
};

export default UserProfile;

这样可以避免在组件内部意外修改 props,提高代码的可维护性和稳定性。

5.2 Redux 状态管理

在 Redux 应用中,状态通常应该是不可变的。Readonly 可以用于定义 Redux 的状态类型。

import { createSlice } from '@reduxjs/toolkit';

type CounterState = Readonly<{
  value: number;
  isLoading: boolean;
}>;

const initialState: CounterState = {
  value: 0,
  isLoading: false
};

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => ({...state, value: state.value + 1 }),
    decrement: (state) => ({...state, value: state.value - 1 })
  }
});

export const { increment, decrement } = counterSlice.actions;
export default counterSlice.reducer;

通过将 CounterState 定义为 Readonly,我们可以确保在 Redux 的 reducer 函数中不会直接修改状态对象,而是通过创建新的对象来更新状态,符合 Redux 的不可变状态原则。

同时,Partial 可以用于定义 Redux action 的 payload 类型。例如,如果我们有一个更新用户信息的 action,payload 可能只包含部分用户属性:

import { createSlice } from '@reduxjs/toolkit';

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

type UpdateUserPayload = Partial<User>;

const userSlice = createSlice({
  name: 'user',
  initialState: {
    name: '',
    age: 0,
    email: ''
  } as User,
  reducers: {
    updateUser: (state, action: { payload: UpdateUserPayload }) => {
      return {...state,...action.payload };
    }
  }
});

export const { updateUser } = userSlice.actions;
export default userSlice.reducer;

这样可以确保 action 的 payload 只包含允许更新的部分用户属性,提高代码的安全性和可维护性。

6. 在 Vue 开发中的应用

6.1 Vue 组件 Props

在 Vue 开发中,PartialReadonly 同样可以用于定义组件的 props

使用 Partial 定义可选 props:对于 Vue 组件,我们可以这样使用 Partial。假设我们有一个 Card 组件:

import { defineComponent } from 'vue';

type CardProps = {
  title: string;
  content: string;
  color?: string;
};

export default defineComponent({
  props: {
    title: { type: String, required: true },
    content: { type: String, required: true },
    color: { type: String }
  } as Partial<CardProps>,
  setup(props) {
    return {
      // 组件逻辑
    };
  }
});

这里通过 as Partial<CardProps> 明确了 color 属性是可选的,并且 Vue 的类型检查会根据这个类型定义进行。

使用 Readonly 确保 props 不可变:虽然 Vue 本身有一些机制来防止直接修改 props,但使用 Readonly 可以在类型层面提供额外的保障。

import { defineComponent } from 'vue';

type ImageProps = Readonly<{
  src: string;
  alt: string;
}>;

export default defineComponent({
  props: {
    src: { type: String, required: true },
    alt: { type: String, required: true }
  } as ImageProps,
  setup(props) {
    // 以下操作会报错,因为 props 是只读的
    // props.src = 'new-src.jpg'; 
    return {
      // 组件逻辑
    };
  }
});

这样可以避免在组件内部意外修改 props,使代码更加健壮。

6.2 Vuex 状态管理

在 Vuex 应用中,状态也应该是不可变的。Readonly 可以用于定义 Vuex 的状态类型。

import { createStore } from 'vuex';

type CartState = Readonly<{
  items: { name: string; price: number }[];
  total: number;
}>;

const state: CartState = {
  items: [],
  total: 0
};

const store = createStore({
  state,
  mutations: {
    addItem(state, item: { name: string; price: number }) {
      return {
      ...state,
        items: [...state.items, item],
        total: state.total + item.price
      };
    }
  }
});

export default store;

通过将 CartState 定义为 Readonly,我们确保在 Vuex 的 mutation 函数中不会直接修改状态对象,而是通过创建新的对象来更新状态,符合 Vuex 的不可变状态原则。

同时,Partial 可以用于定义 Vuex action 的 payload 类型。例如,如果我们有一个更新购物车中商品数量的 action,payload 可能只包含商品的索引和新的数量:

import { createStore } from 'vuex';

type CartItem = { name: string; price: number; quantity: number };
type UpdateQuantityPayload = Partial<{ index: number; quantity: number }>;

const store = createStore({
  state: {
    items: [] as CartItem[],
    total: 0
  },
  mutations: {
    updateQuantity(state, payload: UpdateQuantityPayload) {
      if (payload.index!== undefined && payload.quantity!== undefined) {
        const newItems = [...state.items];
        newItems[payload.index].quantity = payload.quantity;
        return {
        ...state,
          items: newItems,
          total: newItems.reduce((acc, item) => acc + item.price * item.quantity, 0)
        };
      }
      return state;
    }
  }
});

export default store;

这样可以确保 action 的 payload 只包含允许更新的部分数据,提高代码的安全性和可维护性。

7. 总结与注意事项

通过以上的介绍,我们深入了解了 PartialReadonly 映射类型在前端开发中的各种应用场景,包括 React 和 Vue 等框架以及状态管理库中的使用。

需要注意的是,虽然 PartialReadonly 在类型层面提供了强大的功能,但它们并不能完全替代运行时的检查。在一些关键的业务逻辑中,仍然需要进行运行时的验证,以确保数据的完整性和正确性。

另外,在使用复杂类型组合 PartialReadonly 时,要确保类型定义的清晰和可读性。过度复杂的类型组合可能会导致代码难以理解和维护,所以要根据实际情况合理使用。

总的来说,PartialReadonly 是 TypeScript 中非常实用的映射类型,掌握它们的使用可以提高前端代码的质量和可维护性。在日常开发中,我们应该根据具体的业务需求,灵活运用这两个类型,为我们的项目带来更多的优势。