You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用可辨识联合类型默认值时,TypeScript类型推断错误

如何让TypeScript在React组件默认形态下严格校验props

我实现了一个支持按钮和链接两种形态的React Button组件,用TypeScript做类型约束,核心代码如下:

export type BaseProps = {
    size?: 'xs' | 'sm' | 'md' | 'lg';
}

type ButtonAsButtonProps = Omit<React.ComponentPropsWithoutRef<'button'>, 'as'> & BaseProps & {
    as?: 'button';
}

type ButtonAsLinkProps = Omit<React.ComponentPropsWithoutRef<'a'>, 'as'> & BaseProps & {
    as: 'link';
    link: string;
}

type ButtonProps = ButtonAsButtonProps | ButtonAsLinkProps

const Button = ( props : ButtonProps ) => {
    
    props.as = props.as ?? 'button'

    if ( props.as === 'link' ) {
        return <Link { ... props } />
    }
    
    return <Box<'button'> as='button' { ... props } />
}

export default Button

当显式指定as属性时,类型校验符合预期:

  • <Button as='button' link='' disabled />会报错link不属于按钮props
  • <Button as='link' link='' disabled />会报错disabled不属于链接props

但当不指定as(默认使用按钮形态)时,传入link、href等链接专属属性,TypeScript却不报错:

// 期望报错,但实际无任何提示
<Button link='' href='' target='' onClick={() => {}} disabled />

这是因为联合类型ButtonProps允许传入同时满足两个分支的交叉类型——只要传入的属性集合属于任意一个分支的超集,TypeScript就会认为它符合联合类型。而我需要让默认形态下只能传入按钮专属的props,禁止链接相关属性。


解决方案

方案1:给按钮分支添加专属属性的禁止约束

在ButtonAsButtonProps中显式禁止链接分支的专属属性(比如link),让两个分支的属性互斥:

export type BaseProps = {
    size?: 'xs' | 'sm' | 'md' | 'lg';
}

type ButtonAsButtonProps = Omit<React.ComponentPropsWithoutRef<'button'>, 'as'> & BaseProps & {
    as?: 'button';
    // 禁止link属性,确保按钮形态下无法传入该属性
    link?: never;
}

type ButtonAsLinkProps = Omit<React.ComponentPropsWithoutRef<'a'>, 'as'> & BaseProps & {
    as: 'link';
    link: string;
}

type ButtonProps = ButtonAsButtonProps | ButtonAsLinkProps

const Button = (props: ButtonProps) => {
    const resolvedAs = props.as ?? 'button';

    if (resolvedAs === 'link') {
        return <Link {...props as ButtonAsLinkProps} />;
    }

    return <Box<'button'> as='button' {...props as ButtonAsButtonProps} />;
}

export default Button

此时,当不指定as并传入link时,TypeScript会发现该属性违反了ButtonAsButtonProps的约束,同时又不满足ButtonAsLinkProps必须传入as='link'的要求,从而抛出正确的类型错误。

方案2:拆分联合类型,明确默认分支

将默认形态(不指定as)单独作为一个类型分支,与显式指定as的分支组成联合,让TypeScript在无as时只能匹配按钮类型:

export type BaseProps = {
    size?: 'xs' | 'sm' | 'md' | 'lg';
}

// 默认形态:不指定as,仅允许按钮相关props
type ButtonDefaultProps = Omit<React.ComponentPropsWithoutRef<'button'>, 'as'> & BaseProps;

// 显式指定as='button'的形态
type ButtonAsButtonProps = ButtonDefaultProps & { as: 'button' };

// 链接形态
type ButtonAsLinkProps = Omit<React.ComponentPropsWithoutRef<'a'>, 'as'> & BaseProps & {
    as: 'link';
    link: string;
};

// 联合类型覆盖所有场景
type ButtonProps = ButtonDefaultProps | ButtonAsButtonProps | ButtonAsLinkProps;

const Button = (props: ButtonProps) => {
    const resolvedAs = 'as' in props ? props.as : 'button';

    if (resolvedAs === 'link') {
        return <Link {...props as ButtonAsLinkProps} />;
    }

    return <Box<'button'> as='button' {...props as ButtonDefaultProps} />;
}

export default Button

这种方式更清晰地划分了不同场景的类型,默认形态下只能传入按钮props,传入link、href等属性会直接触发类型错误。

方案3:使用函数重载

利用TypeScript函数重载特性,为不同入参场景定义精准的类型约束:

import React from 'react';

export type BaseProps = {
    size?: 'xs' | 'sm' | 'md' | 'lg';
}

type ButtonAsButtonProps = Omit<React.ComponentPropsWithoutRef<'button'>, 'as'> & BaseProps & {
    as?: 'button';
}

type ButtonAsLinkProps = Omit<React.ComponentPropsWithoutRef<'a'>, 'as'> & BaseProps & {
    as: 'link';
    link: string;
}

// 重载签名1:默认形态,无as属性
function Button(props: Omit<ButtonAsButtonProps, 'as'>): JSX.Element;
// 重载签名2:显式指定as='button'
function Button(props: ButtonAsButtonProps): JSX.Element;
// 重载签名3:显式指定as='link'
function Button(props: ButtonAsLinkProps): JSX.Element;

// 组件实现
function Button(props: ButtonAsButtonProps | ButtonAsLinkProps) {
    const resolvedAs = props.as ?? 'button';

    if (resolvedAs === 'link') {
        return <Link {...props as ButtonAsLinkProps} />;
    }

    return <Box<'button'> as='button' {...props as ButtonAsButtonProps} />;
}

export default Button

函数重载可以直接为默认场景定义严格的类型,确保不指定as时只能传入按钮相关props,避免联合类型的模糊推断问题。


核心原因

原来的联合类型ButtonProps允许传入同时满足两个分支的交叉类型(比如同时包含按钮和链接的props),因为交叉类型是联合类型的子类型。通过让分支属性互斥、拆分默认分支或使用函数重载,可以让TypeScript在默认场景下精准匹配按钮类型,实现严格的props校验。

内容的提问来源于stack exchange,提问作者grazdev

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 20:27:06