正在加载,请稍候…

TypeScript 实用类型:Partial、Required、Pick、Omit 及更多

通过实际示例掌握 TypeScript 内置实用类型,学习 Partial、Required、Readonly、Pick、Omit、Record、Exclude

TypeScript Utility Types: Partial, Required, Pick, Omit and More

什么是实用类型?

TypeScript 提供了一组泛型实用类型,用于将现有类型转换为新类型。它们消除了重复的类型声明,让你无需重复即可从现有类型派生新类型。

所有实用类型都内置于 TypeScript 中——无需导入。

TypeScript Utility Types: Partial, Required, Pick, Omit and More illustration

Partial

将 T 的所有属性变为可选。

interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user';
}

type PartialUser = Partial<User>;
// {
//   id?: number;
//   name?: string;
//   email?: string;
//   role?: 'admin' | 'user';
// }

// 常见用例:接受部分数据的更新函数
async function updateUser(id: number, updates: Partial<User>) {
  return await db.users.update({ where: { id }, data: updates });
}

updateUser(1, { name: 'Alice' });           // ✅ 只更新 name
updateUser(1, { email: 'new@example.com' }); // ✅ 只更新 email

Required

与 Partial 相反——将所有可选属性变为必选。

interface Config {
  host?: string;
  port?: number;
  database?: string;
}

type RequiredConfig = Required<Config>;
// { host: string; port: number; database: string; }

function connect(config: RequiredConfig) {
  // 所有字段保证存在
}

Readonly

将所有属性变为初始化后不可赋值。

type ReadonlyUser = Readonly<User>;

const user: ReadonlyUser = { id: 1, name: 'Alice', email: 'a@b.com', role: 'user' };
user.name = 'Bob'; // ❌ TypeScript 错误:无法分配到 'name',因为它是只读属性

// 深度只读(Readonly 是浅层的)
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

Pick<T, K>

从 T 中选取指定键创建类型。

type UserPreview = Pick<User, 'id' | 'name'>;
// { id: number; name: string; }

// 用例:仅暴露安全字段的 API 响应类型
type PublicUser = Pick<User, 'id' | 'name'>;  // 从公共 API 中排除 email 和 role

function getUserProfile(id: number): Promise<PublicUser> {
  return db.users.findOne({ where: { id }, select: ['id', 'name'] });
}

TypeScript Utility Types: Partial, Required, Pick, Omit and More illustration

Omit<T, K>

从 T 中排除指定键创建类型——与 Pick 相反。

type UserWithoutId = Omit<User, 'id'>;
// { name: string; email: string; role: 'admin' | 'user'; }

// 用例:创建负载(在数据库分配 id 之前)
type CreateUserPayload = Omit<User, 'id'>;

async function createUser(payload: CreateUserPayload): Promise<User> {
  return await db.users.create({ data: payload });
}

// 排除多个键
type UserFormData = Omit<User, 'id' | 'role'>;

Record<K, V>

创建键为 K、值为 V 的对象类型。

// 从字符串到数字的映射
type ScoreMap = Record<string, number>;
const scores: ScoreMap = { alice: 100, bob: 95, carol: 87 };

// 从联合类型到对象的映射
type UserRole = 'admin' | 'editor' | 'viewer';

type RolePermissions = Record<UserRole, { canEdit: boolean; canDelete: boolean }>;

const permissions: RolePermissions = {
  admin:  { canEdit: true,  canDelete: true  },
  editor: { canEdit: true,  canDelete: false },
  viewer: { canEdit: false, canDelete: false },
};

// 类型安全的查找
function getPermissions(role: UserRole) {
  return permissions[role]; // TypeScript 知道这个始终有值
}

Exclude<T, U>

从 T 中移除可赋值给 U 的类型。

type Status = 'active' | 'inactive' | 'deleted' | 'banned';

type ActiveStatus = Exclude<Status, 'deleted' | 'banned'>;
// 'active' | 'inactive'

// 常用于联合类型
type NonNullable<T> = Exclude<T, null | undefined>;  // 这实际上是内置的!

type StringOrNull = string | null | undefined;
type JustString = Exclude<StringOrNull, null | undefined>;  // string

Extract<T, U>

仅保留 T 中可赋值给 U 的类型(与 Exclude 相反)。

type Mixed = string | number | boolean | null;

type JustStrings = Extract<Mixed, string | number>;  // string | number
type Primitives = Extract<Mixed, string | number | boolean>;  // string | number | boolean

TypeScript Utility Types: Partial, Required, Pick, Omit and More illustration

ReturnType

提取函数类型的返回类型。

function getUser() {
  return { id: 1, name: 'Alice', email: 'alice@example.com' };
}

type GetUserReturn = ReturnType<typeof getUser>;
// { id: number; name: string; email: string; }

// 适用于异步函数(返回 Promise<T>)
async function fetchData() {
  return { users: [] as User[], total: 0 };
}

type FetchDataReturn = Awaited<ReturnType<typeof fetchData>>;
// { users: User[], total: number }

Parameters

以元组形式提取函数参数类型。

function createPost(title: string, content: string, authorId: number) {
  // ...
}

type CreatePostParams = Parameters<typeof createPost>;
// [title: string, content: string, authorId: number]

// 用于包装函数
function loggedCreatePost(...args: Parameters<typeof createPost>) {
  console.log('Creating post:', args[0]);
  return createPost(...args);
}

NonNullable

从 T 中移除 null 和 undefined。

type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>;  // string

// 与 API 响应的实际用法
interface ApiResponse {
  user: User | null;
}

function processUser(response: ApiResponse) {
  if (response.user === null) return;
  const user: NonNullable<typeof response.user> = response.user;  // User (非 null)
}

组合实用类型

真正的威力来自于组合使用:

interface Product {
  id: string;
  name: string;
  price: number;
  description: string;
  imageUrl: string;
  createdAt: Date;
  updatedAt: Date;
  ownerId: string;
}

// 创建类型:无 id、时间戳或 ownerId(由服务器设置)
type CreateProductInput = Omit<Product, 'id' | 'createdAt' | 'updatedAt' | 'ownerId'>;

// 更新类型:与创建相同的字段,但全部可选
type UpdateProductInput = Partial<CreateProductInput>;

// 列表类型:仅预览字段
type ProductSummary = Pick<Product, 'id' | 'name' | 'price' | 'imageUrl'>;

// 管理员视图类型:全部只读
type AdminProduct = Readonly<Product>;

常见问题

问:Pick 和 Omit 有什么区别? Pick 白名单列出你想保留的字段。Omit 黑名单列出你想移除的字段。当你想要一个小子集时使用 Pick;当你想要除了少数几个字段之外的所有字段时使用 Omit。

问:什么时候应该使用 Partial 与可选属性? 当字段在基础类型中确实是可选时,使用可选属性(?)。当你需要一个通常全部必选的类型的全可选版本时(如更新负载),使用 Partial<T>

问:Readonly 是深度只读的吗? 不,Readonly<T> 是浅层的。对于深度不可变类型,使用递归的 DeepReadonly 泛型或 as const 断言。

→ 使用 JSON Viewer 检查 TypeScript API 响应和类型结构。