编译绿了,类型检查却挂:React HTMLAttributes 同名 prop 冲突与 Omit 解法
在给一个组件库做类型修整时,我在 tsc --noEmit 里连续看到三条 TS2430,组件分别是 Watermark、Alert、Affix。报错信息格式相同,都指向同一根因——我当时以为是自己的类型写错了,花了点时间才回过神来,问题出在 React 本身。
现象
组件 props 接口继承了 HTMLAttributes<HTMLDivElement>,并新增了一个同名的自定义 prop:
// 报错版本
export interface WatermarkProps extends HTMLAttributes<HTMLDivElement> {
content?: string | string[]; // ❌ TS2430
}TypeScript 的报错是:
error TS2430: Interface 'WatermarkProps' incorrectly extends interface 'HTMLAttributes<HTMLDivElement>'.
Types of property 'content' are incompatible.
Type 'string | string[] | undefined' is not assignable to type 'string | undefined'.
错误读起来像是「你的类型比父接口的要宽,不合法」。没错,但关键是——我根本不知道 HTMLAttributes 里有一个叫 content 的属性。
根因
React 的 HTMLAttributes<T> 暴露了一批 HTML 全局属性,其中就包括 content(微数据属性,类型是 string | undefined)。接口继承有一个硬约束:子接口只能收窄继承成员的类型,不能扩展或改变。
string | string[] 比 string 更宽,TypeScript 拒绝。
esbuild、SWC、Turbopack 在打包时跳过类型检查,只做语法转换。next build 或 vite build 控制台输出 "Compiled successfully" 并不意味着 TypeScript 接口兼容性没问题。
TS2430 只会在 tsc --noEmit 或 Next.js 独立的 "Running TypeScript..." 步骤里出现。如果 CI 只跑构建、不跑 typecheck,这类错误会悄悄带进生产包。
常见的撞名高危词(HTMLAttributes 或其子集里已声明):
| 属性名 | 原生类型 | 常见冲突场景 |
|--------|---------|-------------|
| content | string | 水印文字、卡片内容(string[] 或 ReactNode) |
| title | string | Alert / Card 标题(ReactNode) |
| color | string | 主题色 "primary" \| "danger" 联合类型 |
| onChange | ChangeEventHandler<T> | 自定义回调 (value: X) => void |
| width / height / size | number | 设计系统尺寸枚举 |
正解
用 Omit 在继承前把冲突的键剔掉,再用自己的类型重新声明:
import type { HTMLAttributes, ReactNode } from "react";
// Omit "content":React HTMLAttributes 的 content 是微数据 string,
// 此处需要 string | string[],故先剔除再重声明。
export interface WatermarkProps
extends Omit<HTMLAttributes<HTMLDivElement>, "content"> {
content?: string | string[];
}多个字段同时冲突时,联合剔除:
extends Omit<HTMLAttributes<HTMLDivElement>, "content" | "title" | "color">留一行注释说明 Omit 的原因——否则下一个读代码的人会以为是多余的,顺手删掉。
Omit<HTMLAttributes<HTMLDivElement>, "content"> 把 React 内置的 content?: string 从继承链里移除,之后 WatermarkProps.content 可以自由定义为任意类型,不再受父接口约束。
改名(如 text 代替 content)能绕开冲突,但会破坏已有的调用方 API。如果这个 prop 名是对外契约的一部分,不能随意换——Omit 是更保守、更精确的修法。@ts-expect-error 绝对不要用:它隐藏了类型不兼容,反而让调用方误以为可以传原生 HTML 属性。
一句话外卖
凡是
extends HTMLAttributes(或其衍生接口),自定义 prop 起名前先去@types/react确认无撞名;撞了就Omit,不要改名,不要@ts-expect-error。