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对联合类型的校验逻辑是:只要传入的值能匹配联合中的任意一个分支,就判定为合法。这里的关键问题在于:
- 函数类型的兼容性:
UserFormHandler<EditUser>(接收EditUser的函数)可以赋值给UserFormHandler<NewUser>(接收NewUser的函数)。因为TS中函数参数是逆变的——NewUser是EditUser的子类型(包含EditUser的所有属性),所以接受父类型参数的函数可以兼容接受子类型参数的函数。 - 未传入
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
相关产品推荐
相关产品推荐

