在组件 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: ... }
A & B 对同名字段取交集,如果类型不同会产生 never。适用场景是两个类型没有同名字段冲突的情况。Omit<A, keyof B> 去掉 A 中与 B 冲突的字段,再与 B 交叉。这样避免同名字段的类型冲突。// 实战:配置覆盖模式
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 独有字段