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

React组件Props不符合类型约束却未触发TypeScript类型错误的原因与解决

React组件联合Props未触发TypeScript类型错误的原因与解决方法

问题回顾

在普通TypeScript场景中,联合类型能严格校验对象字面量的合法性:

type A = {
  kind: 'a'
  other?: undefined
}

type B = {
  kind: 'b'
  other: number
}

type Kind = A | B

function inspect(obj: Kind) {}

inspect({ kind: 'a', other: 44 }) // 报错:kind为'a'时other不能设为数字
inspect({ kind: 'b' }) // 报错:kind为'b'时必须传入other
inspect({ kind: 'b', other: 44 }) // 正常

但在React组件场景中,定义联合Props后,传入不符合预期的参数却未触发错误:

import React, { FC } from 'react'

type User = { name: string; id: string }
type NewUser = { name: string; pswd: string }
type EditUser = { name: string }

type UserFormHandler<T> = (data: T) => Promise<void>

type NewUserFormProps = {
  onSubmit: UserFormHandler<NewUser>
  user?: undefined
}

type EditUserFormProps = {
  onSubmit: UserFormHandler<EditUser>
  user: User
}

type UserFormProps = NewUserFormProps | EditUserFormProps

const UserForm: FC<UserFormProps> = props => null

const Usage: FC = () => {
  const onSubmit: UserFormHandler<EditUser> = () => Promise.resolve()
  return <UserForm onSubmit={onSubmit} /> // 未报错:未传入Edit场景必填的user
}

原因分析

TypeScript对联合类型的校验逻辑是:只要传入的值能匹配联合中的任意一个分支,就判定为合法。这里的关键问题在于:

  1. 函数类型的兼容性:UserFormHandler<EditUser>(接收EditUser的函数)可以赋值给UserFormHandler<NewUser>(接收NewUser的函数)。因为TS中函数参数是逆变的——NewUser是EditUser的子类型(包含EditUser的所有属性),所以接受父类型参数的函数可以兼容接受子类型参数的函数。
  2. 未传入user属性时,符合NewUserFormProps中user?: undefined的定义(不传属性等价于赋值为undefined)。

综上,TS认为<UserForm onSubmit={onSubmit} />的props完全匹配NewUserFormProps分支,因此没有触发错误,但这和我们的业务意图(onSubmit为Edit类型时必须传user)不符。

解决方法

方法一:添加可区分的字面量标记(推荐)

给两个Props分支添加唯一的字面量字段(类似普通TS例子中的kind),让TS能明确区分分支,从而严格校验对应分支的必填属性:

type NewUserFormProps = {
  formType: 'new', // 新增可区分标记
  onSubmit: UserFormHandler<NewUser>
  user?: undefined
}

type EditUserFormProps = {
  formType: 'edit', // 新增可区分标记
  onSubmit: UserFormHandler<EditUser>
  user: User
}

type UserFormProps = NewUserFormProps | EditUserFormProps

// 使用时:
const Usage: FC = () => {
  const onSubmitEdit: UserFormHandler<EditUser> = () => Promise.resolve()
  // 报错:缺少必填的user属性
  return <UserForm formType="edit" onSubmit={onSubmitEdit} /> 
}

方法二:通过品牌类型排除分支兼容性

给每个Handler添加品牌类型,避免跨分支兼容,强制TS匹配对应分支的所有属性:

// 给每个Handler添加品牌类型,避免跨分支兼容
type NewUserHandler = UserFormHandler<NewUser> & { __brand: 'new' }
type EditUserHandler = UserFormHandler<EditUser> & { __brand: 'edit' }

type NewUserFormProps = {
  onSubmit: NewUserHandler
  user?: undefined
}

type EditUserFormProps = {
  onSubmit: EditUserHandler
  user: User
}

type UserFormProps = NewUserFormProps | EditUserFormProps

// 使用时:
const Usage: FC = () => {
  const onSubmitEdit: EditUserHandler = (() => Promise.resolve()) as EditUserHandler
  // 报错:缺少user属性
  return <UserForm onSubmit={onSubmitEdit} /> 
}

方法三:使用精确类型校验

通过工具类型实现精确匹配,确保传入的props完全符合某一个分支,不能只匹配部分属性:

// 实现精确类型工具
type Exact<T, U> = T extends U ? (U extends T ? T : never) : never;

// 重新定义联合类型
type UserFormProps = Exact<NewUserFormProps, any> | Exact<EditUserFormProps, any>

// 使用时,未传user的Edit场景会报错
const Usage: FC = () => {
  const onSubmitEdit: UserFormHandler<EditUser> = () => Promise.resolve()
  return <UserForm onSubmit={onSubmitEdit} /> // 报错
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 02:20:58