返回博客

编译绿了,类型检查却挂: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 buildvite 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 剔除冲突键再重声明

Omit<HTMLAttributes<HTMLDivElement>, "content"> 把 React 内置的 content?: string 从继承链里移除,之后 WatermarkProps.content 可以自由定义为任意类型,不再受父接口约束。

为什么不重命名 prop?

改名(如 text 代替 content)能绕开冲突,但会破坏已有的调用方 API。如果这个 prop 名是对外契约的一部分,不能随意换——Omit 是更保守、更精确的修法。@ts-expect-error 绝对不要用:它隐藏了类型不兼容,反而让调用方误以为可以传原生 HTML 属性。

一句话外卖

凡是 extends HTMLAttributes(或其衍生接口),自定义 prop 起名前先去 @types/react 确认无撞名;撞了就 Omit,不要改名,不要 @ts-expect-error