HeroUI v3 的 variant 不向后兼容:从 NextUI v2 迁移时所有 variant= 都是嫌疑人
把一个 React 项目从 NextUI v2 升级到 HeroUI v3,大多数组件名没动(Card、Chip、Button、Badge……),看起来是无痛升级。但跑 tsc --noEmit 时,迎面而来的是一堆 TS2322——每一行都在说某个 variant 值不合法。
这不是类型定义写错了,是整个 variant 命名空间被重新设计了。
现象
升级 @heroui/react 到 ^3.0.0 后,原本在 NextUI v2 下工作良好的代码:
<Chip size="sm" variant="flat">{label}</Chip>
<Button variant="bordered">取消</Button>
<Card variant="shadow">...</Card>在 tsc --noEmit 时全部报错:
error TS2322: Type '"flat"' is not assignable to type
'"secondary" | "tertiary" | "primary" | "soft" | undefined'.
error TS2322: Type '"bordered"' is not assignable to type
'"primary" | "secondary" | "tertiary" | "outline" | "ghost" | "danger" | undefined'.
根因
HeroUI v3 对 variant 做了一次概念重构,不是改名,是换维度:
- NextUI v2:variant 表达视觉处理(solid / bordered / light / flat / faded / shadow / ghost)
- HeroUI v3:variant 表达语义角色(primary / secondary / tertiary / soft / danger / success / warning)
更麻烦的是,HeroUI v3 的 variant 集合每个组件各不相同,没有全局共享枚举。ChipVariants["variant"] 和 CardVariants["variant"] 是两个互不相交的 literal union。
AI 训练数据大量来自 NextUI v2 代码,会自信地补全 variant="flat"、variant="solid"——这些在 HeroUI v3 里全部不存在。Stack Overflow 上的旧答案同理。唯一的真相在组件的 .d.ts 里。
v2 → v3 常用 variant 对照
| NextUI v2 旧值 | HeroUI v3 最近似替代 |
|---|---|
| variant="solid" | variant="primary"(Chip/Button),或直接省略(多数是默认值) |
| variant="flat" | variant="soft"(Chip/Badge) |
| variant="bordered" | variant="outline"(Button),Chip 无对应——用 soft 或去掉 |
| variant="light" | variant="ghost"(Button),variant="soft"(Chip) |
| variant="faded" | variant="tertiary"(粗略近似) |
| variant="shadow" | 已删除——改用 Tailwind className="shadow-*" 手动加 |
| variant="ghost" | 仅 Button 保留,其他组件已删 |
各组件 v3 可用 variant 一览(@heroui/styles v3.1.0 验证):
| 组件 | v3 可用 variant | 默认 |
|---|---|---|
| Card | default secondary tertiary transparent | default |
| Chip | primary secondary tertiary soft | secondary |
| Button | primary secondary tertiary outline ghost danger | primary |
| Input | primary secondary | primary |
正解
迁移前验证,而不是迁移后猜测。 三种查法:
# 1. 看 tailwind-variants 配置(最完整)
cat node_modules/@heroui/styles/dist/components/chip/chip.styles.js | grep -A20 "variant:"
# 2. 看生成的类型定义
grep -A5 "variant:" node_modules/@heroui/styles/dist/components/chip/chip.styles.d.ts
# 3. 直接写一个错的,让 tsc 告诉你全集(最快)
npx tsc --noEmit 2>&1 | grep "is not assignable"第 3 种最省事——TypeScript 的错误信息本身就包含完整的 allowed union literal,读一次就知道所有选项。
不要做字符串替换,要问「我想表达什么」:
- 想要一个低调的标签 →
<Chip variant="soft"> - 想要主要行动按钮 →
<Button variant="primary">(或直接省略) - 想要无阴影的卡片 →
<Card variant="transparent">或<Card variant="secondary">
每换完一个组件就跑一次 tsc --noEmit,不要批量换完再统一调试。
别漏掉 color 和 variant 的区别
HeroUI v3 里不少组件同时有 color 和 variant 两个 prop,两者是独立的 literal union,都会独立报 typecheck 错误:
// 合法:variant 控制视觉处理,color 控制调色板
<Chip variant="soft" color="danger">删除</Chip>
// 非法:danger 不在 Chip 的 variant 集合里
<Chip variant="danger">删除</Chip>迁移时如果原来用 variant="solid" color="danger",不能只改 variant 不查 color。
迁移后的扫尾
改完之后,跑一遍 grep 找可能被 any 掩盖的漏网之鱼:
git grep -nE 'variant="(solid|bordered|light|flat|faded|shadow|dot)"' src/any 类型会让这些错误在 typecheck 时静默通过,但运行时样式会对不上。
一句话外卖
HeroUI v3 不是 NextUI v2 的包名替换,是一次 API 重设计——所有带
variant=的地方都是嫌疑人,真相在.d.ts,不在记忆里。