← 返回导航

Merge 与 Overwrite 类型合并

工具类型 · 中级

场景说明

在组件 Props 合并、配置覆盖、选项合并等场景中,经常需要将两个类型合并,且后者的同名字段覆盖前者。直接用 & 交叉类型无法满足"覆盖"语义,因为同名字段如果类型不同会产生 never 而非覆盖。这时需要自定义 Merge 和 Overwrite 工具类型。

完整代码


      
// Merge 与 Overwrite:类型合并的正确姿势

// 1. & 交叉类型的问题
type A = { name: string; age: number };
type B = { name: number; email: string };
type AB = A & B;
// AB = { name: string & number; age: number; email: string }
// name 是 string & number = never,无法使用!

// 2. Merge<A, B> -- 合并两个类型,B 的同名字段覆盖 A
type Merge<A, B> = {
  [K in keyof A | keyof B]: K extends keyof B
    ? B[K]
    : K extends keyof A
      ? A[K]
      : never;
};

type Merged = Merge<A, B>;
// { name: number; age: number; email: string } -- B 的 name 覆盖了 A 的 name

// 3. Overwrite<A, B> -- 用 B 覆盖 A(B 优先)
type Overwrite<A, B> = Omit<A, keyof B> & B;

type Overwritten = Overwrite<A, B>;
// { age: number; name: number; email: string }
// 先用 Omit 去掉 A 中 B 也有的 key,再与 B 交叉

// 实战:组件 Props 合并
interface BaseProps {
  size: 'small' | 'medium';
  disabled: boolean;
  className: string;
}

interface ButtonProps {
  variant: 'primary' | 'secondary';
  disabled: boolean;  // 与 BaseProps 同名,但这里想覆盖描述
  onClick: () => void;
}

type FullButtonProps = Merge<BaseProps, ButtonProps>;
// { size: ...; disabled: boolean; className: string; variant: ...; onClick: ... }

关键点解析

使用示例与类型推导


      
// 实战:配置覆盖模式

interface DefaultConfig {
  host: string;
  port: number;
  timeout: number;
  retry: number;
  debug: boolean;
}

const defaults: DefaultConfig = {
  host: 'localhost',
  port: 3000,
  timeout: 5000,
  retry: 3,
  debug: false,
};

type UserConfig = Partial<DefaultConfig>;

function createConfig(userConfig: UserConfig): DefaultConfig {
  // 合并:defaults 为基础,userConfig 覆盖
  return { ...defaults, ...userConfig };
}

const config = createConfig({ port: 8080, debug: true });
// { host: 'localhost', port: 8080, timeout: 5000, retry: 3, debug: true }

// 类型层面:推导合并后的结果
type FinalConfig = Merge<DefaultConfig, UserConfig>;
// UserConfig 覆盖同名字段,保留 DefaultConfig 独有字段
在 Playground 中尝试