基于Next.js Link与TypeScript的多态Button组件类型错误修复
问题背景
我正在构建一个多态Button组件,可根据as属性渲染Next.js的Link或原生button元素。现有代码如下:
import { ComponentPropsWithoutRef, ElementType, ReactNode } from "react"; import Link from "next/link"; type OwnProps<T extends ElementType = ElementType> = { as?: T; variant?: "primary" | "secondary" | "neutral" | "danger" | "text"; }; type Props<T extends ElementType> = OwnProps<T> & Omit<ComponentPropsWithoutRef<T>, keyof OwnProps>; export default function Button<T extends ElementType = "button" | "a">({ as, variant = "primary", ...props }: Props<T>) { const Component = as === "a" ? Link : "button"; return <Component {...props} />; }
代码可正常运行编译,但TypeScript在<Component {...props} />处抛出如下错误:
Type 'Omit<Props<T>, "as" | "variant">' is not assignable to type 'IntrinsicAttributes & Omit<AnchorHTMLAttributes<HTMLAnchorElement>, keyof InternalLinkProps> & ... 4 more ... & ButtonHTMLAttributes<...>'. Property 'href' is missing in type 'Omit<Props<T>, "as" | "variant">' but required in type 'InternalLinkProps'.ts(2322) link.d.ts(11, 5): 'href' is declared here.
问题核心:Next.js的LinkProps要求href为必填项,但当前类型定义中该属性可能为undefined(因为button元素不需要它)。
最佳修复方案
方案1:条件类型约束(推荐)
通过条件类型让TypeScript根据as的值自动校验必填属性,修改类型定义:
import { ComponentPropsWithoutRef, ElementType } from "react"; import Link from "next/link"; import type { LinkProps } from "next/link"; type OwnProps = { variant?: "primary" | "secondary" | "neutral" | "danger" | "text"; }; // 针对不同的as类型定义对应的props type Props<T extends ElementType> = T extends "a" ? OwnProps & Omit<LinkProps, keyof OwnProps> : T extends "button" ? OwnProps & Omit<ComponentPropsWithoutRef<"button">, keyof OwnProps> : OwnProps & Omit<ComponentPropsWithoutRef<T>, keyof OwnProps>; export default function Button<T extends ElementType = "button">({ as, variant = "primary", ...props }: Props<T>) { const Component = as === "a" ? Link : as ?? "button"; return <Component {...props as any} />; }
说明:
- 当
as为"a"时,Props会继承LinkProps,强制要求传入href - 当
as为"button"时,Props使用原生button的属性,不需要href - 支持扩展其他ElementType,保持组件的多态性
方案2:函数重载
通过函数重载明确不同as对应的props规则,让TypeScript做精准推断:
import { ComponentPropsWithoutRef } from "react"; import Link from "next/link"; import type { LinkProps } from "next/link"; type OwnProps = { variant?: "primary" | "secondary" | "neutral" | "danger" | "text"; }; // 重载1:渲染原生button export default function Button(props: OwnProps & ComponentPropsWithoutRef<"button">): JSX.Element; // 重载2:渲染Next.js Link(对应a标签) export default function Button(props: OwnProps & Omit<LinkProps, "as">): JSX.Element; // 通用实现 export default function Button({ as, variant = "primary", ...props }: any) { const Component = as === "a" ? Link : "button"; return <Component {...props} />; }
说明:
- 为两种核心场景分别定义重载签名,TypeScript会根据传入的props自动匹配对应的类型
- 实现部分使用
any简化类型处理,适合不需要支持大量自定义ElementType的场景
方案3:精准类型断言
如果不想修改原有类型结构,可在组件内部根据as的值做类型断言:
import { ComponentPropsWithoutRef, ElementType } from "react"; import Link from "next/link"; import type { LinkProps } from "next/link"; type OwnProps<T extends ElementType = ElementType> = { as?: T; variant?: "primary" | "secondary" | "neutral" | "danger" | "text"; }; type Props<T extends ElementType> = OwnProps<T> & Omit<ComponentPropsWithoutRef<T>, keyof OwnProps>; export default function Button<T extends ElementType = "button" | "a">({ as, variant = "primary", ...props }: Props<T>) { if (as === "a") { return <Link {...props as LinkProps} />; } return <button {...props as ComponentPropsWithoutRef<"button">} />; }
说明:
- 通过分支判断分别处理Link和button的渲染,同时用类型断言告诉TypeScript当前props符合对应组件的要求
- 这种方式代码改动最小,但需要确保分支逻辑和断言的类型一致,避免运行时错误
内容的提问来源于stack exchange,提问作者Andreas Remdt
相关产品推荐
相关产品推荐

