··2 阅读·预计 12 分钟

TypeScript 类型体操实战:手把手实现 DeepPartial 和 DeepRequired

日常开发中,我们常用 Partial<T>Required<T> 来做对象类型的可选/必选转换。但它们只作用于第一层属性,嵌套对象就无能为力了。今天来手写 DeepPartialDeepRequired,顺便聊聊递归类型、条件类型、映射类型这几个 TypeScript 类型系统的核心武器。


问题场景

interface Config {
  db: {
    host: string;
    port: number;
    credentials: {
      username: string;
      password: string;
    };
  };
  cache: {
    ttl: number;
    strategy: 'memory' | 'redis';
  };
}

// Partial 只影响第一层
type ShallowPartial = Partial<Config>;
// {
//   db?: { host: string; port: number; credentials: { ... } };
//   cache?: { ttl: number; strategy: 'memory' | 'redis' };
// }

// db.credentials 里的字段还是必选的!
// 我们希望的是:所有层级都变成可选

实现 DeepPartial

核心思路:递归 + 条件类型

type DeepPartial<T> = T extends object
  ? {
      [K in keyof T]?: DeepPartial<T[K]>;
    }
  : T;

解析:

  • T extends object:判断当前类型是否是对象类型
  • [K in keyof T]?::遍历所有属性,加 ? 使其可选
  • DeepPartial<T[K]>:对每个属性值递归处理
  • : T:非对象类型(string、number、boolean 等)直接返回

验证效果:

type DeepPartialConfig = DeepPartial<Config>;
// {
//   db?: {
//     host?: string;
//     port?: number;
//     credentials?: {
//       username?: string;
//       password?: string;
//     };
//   };
//   cache?: {
//     ttl?: number;
//     strategy?: 'memory' | 'redis';
//   };
// }

完美!所有层级都变成可选了。


实现 DeepRequired

反向操作,把所有可选属性变必选:

type DeepRequired<T> = T extends object
  ? {
      [K in keyof T]-?: DeepRequired<T[K]>;
    }
  : T;

关键区别:-? 表示移除可选标记(removes the optional modifier)。

type ConfigWithOptionals = {
  db?: {
    host?: string;
    port?: number;
  };
  cache?: {
    ttl?: number;
    strategy?: 'memory' | 'redis';
  };
};

type StrictConfig = DeepRequired<ConfigWithOptionals>;
// {
//   db: {
//     host: string;
//     port: number;
//   };
//   cache: {
//     ttl: number;
//     strategy: 'memory' | 'redis';
//   };
// }

处理边界情况

1. 排除数组和 Map 等内置对象

原版实现会把 string[] 变成 { [key: number]: DeepPartial<string> },这不对。需要排除:

type DeepPartial<T> = T extends Function
  ? T
  : T extends Array<infer U>
    ? Array<DeepPartial<U>>
    : T extends Map<infer K, infer V>
      ? Map<K, DeepPartial<V>>
      : T extends Set<infer V>
        ? Set<DeepPartial<V>>
        : T extends object
          ? {
              [K in keyof T]?: DeepPartial<T[K]>;
            }
          : T;

2. 排除 class 实例

对于 DateRegExp 等内置类,不应递归展开:

type IsBuiltIn<T> = T extends Date | RegExp | Error | Function ? true : false;

type DeepPartial<T> = IsBuiltIn<T> extends true
  ? T
  : T extends Array<infer U>
    ? Array<DeepPartial<U>>
    : T extends object
      ? {
          [K in keyof T]?: DeepPartial<T[K]>;
        }
      : T;

实战应用:DeepMerge 类型

在实际项目中,我们经常需要合并配置对象。实现一个类型安全的 deepMerge

type DeepMerge<T, U> = {
  [K in keyof T | keyof U]: K extends keyof U
    ? K extends keyof T
      ? T[K] extends object
        ? U[K] extends object
          ? DeepMerge<T[K], U[K]>
          : U[K]
        : U[K]
      : U[K]
    : K extends keyof T
      ? T[K]
      : never;
};

// 使用示例
type BaseConfig = {
  db: { host: string; port: number };
  log: { level: 'info' | 'debug' };
};

type UserConfig = {
  db: { port: number; pool: number };
  log: { format: 'json' | 'text' };
};

type Merged = DeepMerge<BaseConfig, UserConfig>;
// {
//   db: { host: string; port: number; pool: number };
//   log: { level: 'info' | 'debug'; format: 'json' | 'text' };
// }

配合运行时的 deepMerge 函数:

function deepMerge<T extends object, U extends object>(
  target: T,
  source: U
): DeepMerge<T, U> {
  const result = { ...target } as any;

  for (const key in source) {
    if (Object.prototype.hasOwnProperty.call(source, key)) {
      const val = source[key];
      if (val && typeof val === 'object' && !Array.isArray(val)) {
        result[key] = deepMerge(result[key] || {}, val);
      } else {
        result[key] = val;
      }
    }
  }

  return result;
}

// 类型安全的配置合并
const base: BaseConfig = {
  db: { host: 'localhost', port: 5432 },
  log: { level: 'info' },
};

const user: UserConfig = {
  db: { port: 5433, pool: 10 },
  log: { format: 'json' },
};

const config = deepMerge(base, user);
// config.db.host ✅ 来自 base
// config.db.port ✅ 被 user 覆盖
// config.db.pool ✅ 来自 user
// config.log.level ✅ 来自 base
// config.log.format ✅ 来自 user

性能提示

TypeScript 的递归类型有深度限制(默认约 50 层),超过会报 Type instantiation is excessively deep and possibly infinite

处理方式:限制递归深度

type DeepPartial<T, Depth extends number = 5> = Depth extends 0
  ? T
  : T extends object
    ? {
        [K in keyof T]?: DeepPartial<T[K], Prev[Depth]>;
      }
    : T;

type Prev = [never, 0, 1, 2, 3, 4, 5];

这样可以限制递归深度,避免编译器卡死。


总结

  • Partial / Required — 第一层可选/必选,语法 ? / -?
  • DeepPartial / DeepRequired — 递归可选/必选,递归 + 条件类型
  • DeepMerge — 深度合并,联合类型 + 条件 + 递归

类型体操不是炫技,而是在编译期就捕获错误,减少运行时 bug。掌握递归类型、条件类型、映射类型这三板斧,能覆盖 90% 的业务场景。

0 评论

评论区

登录 后参与评论