为React输入组件联合类型添加label后,受控类型约束失效的原因
React受控/非受控组件TypeScript约束失效问题
我希望自定义的React输入组件能像原生<input />一样,支持受控(通过value)或非受控(通过defaultValue)两种模式。用TypeScript实现逻辑后原本一切正常,但添加额外label属性后,value与onChange的必填约束关系被破坏了。
初始正常实现代码
type OnChangeCallback = (newValue: string) => void; interface Uncontrolled { defaultValue?: string; onChange?: OnChangeCallback; } interface Controlled { value: string; onChange: OnChangeCallback; } type Input = Uncontrolled | Controlled; const input1: Input = {}; // OK const input2: Input = { defaultValue: "Hello, world!", }; // OK const input3: Input = { onChange: newValue => console.log(`New value is ${newValue}`), }; // OK const input4: Input = { value: "Hello, world!", }; // Error: Missing onChange, 符合预期 const input5: Input = { value: "Hello, world!", onChange: newValue => console.log(`New value is ${newValue}`), }; // OK
添加label属性后的代码
type Input = (Uncontrolled | Controlled) & { label: string; };
此时TypeScript允许只传value而不传onChange的实例:
const why: Input = { label: "Oh dear", value: "Hello, world!", }; // 为什么现在不报错了?
原因分析
这是TypeScript结构类型系统和联合类型交叉运算的特性导致的:
- 交叉运算
(A | B) & C等价于(A & C) | (B & C),所以新的Input类型实际是(Uncontrolled & {label:string}) | (Controlled & {label:string})。 Uncontrolled & {label:string}这个类型只要求必须有label,对value属性没有限制(原Uncontrolled接口未定义value,结构类型允许额外属性存在)。- 当传入
{label: "Oh dear", value: "Hello, world!"}时,TypeScript会将其匹配到Uncontrolled & {label:string}分支——因为该对象满足分支的所有必填项(label),额外的value属性不会触发类型检查错误,自然也就不会要求必须传入onChange。
解决方法
在Uncontrolled接口中明确禁止value属性,通过value?: never确保非受控模式下不能传入value:
type OnChangeCallback = (newValue: string) => void; interface Uncontrolled { defaultValue?: string; onChange?: OnChangeCallback; value?: never; // 明确禁止非受控模式下传入value } interface Controlled { value: string; onChange: OnChangeCallback; } type Input = (Uncontrolled | Controlled) & { label: string; }; const fixed1: Input = { label: "Fixed", value: "Hello, world!", }; // Error: Missing onChange, 恢复预期约束 const fixed2: Input = { label: "Fixed", value: "Hello, world!", onChange: newValue => console.log(newValue), }; // OK const fixed3: Input = { label: "Fixed", defaultValue: "Hello, world!", }; // OK
内容的提问来源于stack exchange,提问作者Robo Robok
相关产品推荐
相关产品推荐

