TypeScript中从DeepReadonly对象传递HTML属性遇阻求方案
让我来帮你搞定这个类型不兼容的问题——我仔细看了你的代码和类型实现,找到了两个关键的问题点,咱们一步步来解决:
问题回顾
你原本的组件代码运行正常,但当把otherbuttonProps定义为DeepReadonly类型后,因为ButtonHTMLAttributes期望普通数组而非ReadonlyArray,出现了类型错误。你尝试用DeepMutable转换,但没成功,以下是你的代码和类型实现:
原组件代码
type Props = DR<IBtnWrapperPropsBasic> & {otherbuttonProps?: ButtonHTMLAttributes<HTMLButtonElement>} class Btn extends Component<Props, {}> { ... public render() { const {handleClick, stl, otherbuttonProps, ...otherProps} = this.props return ( <button type="button" // @ts-ignore className={css(getNewDeepMutable([style, stl]))} onMouseDown={this.handleClickW} onKeyDown={this.handleKeyDown} {...otherbuttonProps} {...otherProps} /> ) } }
自定义的DeepReadonly/DeepMutable实现
type DeepReadonly<T> = // tslint:disable-next-line: ban-types T extends AnyFunction | Primitive ? T : T extends ReadonlyArray<infer R> ? IDRArray<R> : T extends ReadonlyMap<infer K, infer V> ? IDRMap<K, V> : T extends ReadonlySet<infer ItemType>? ReadonlySetDeep<ItemType>: T extends object ? DRObject<T> : T interface IDRArray<T> extends ReadonlyArray<DeepReadonly<T>> {} type DRObject<T> = { readonly [P in keyof T]: DeepReadonly<T[P]>; } interface IDRMap<K, V> extends ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>> {} interface ReadonlySetDeep<ItemType> extends ReadonlySet<DeepReadonly<ItemType>> {} export type DeepMutable<T> = // tslint:disable-next-line: ban-types T extends AnyFunction | Primitive ? T : T extends ReadonlyArray<infer R> ? IDeepMuttableArray<R> : T extends ReadonlyMap<infer K, infer V> ? IDeepMutableMap<K, V> : T extends ReadonlySet<infer ItemType>? DeepMutableSetDeep<ItemType>: T extends object ? DeepMutableObject<T> : T interface IDeepMuttableArray<T> extends Array<DeepMutable<T>> {} type DeepMutableObject<T> = { readonly [P in keyof T]: DeepMutable<T[P]>; } interface IDeepMutableMap<K, V> extends Map<DeepMutable<K>, DeepMutable<V>> {} interface DeepMutableSetDeep<ItemType> extends Set<DeepMutable<ItemType>> {}
问题分析
你的DeepMutable实现有个致命的小错误:DeepMutableObject里保留了readonly修饰符!这意味着即使你把DeepReadonly的对象转成DeepMutable,对象的属性还是只读的,自然无法和ButtonHTMLAttributes里的普通属性(包括数组类型)兼容。
另外,你的类型判断只匹配了ReadonlyArray,没覆盖普通Array的情况,虽然DeepReadonly会把普通数组转成只读,但转换回去的时候需要更全面的匹配,确保所有数组类型都能正确转为可变数组。
解决方案
1. 修复DeepMutable的对象类型定义
首先,把DeepMutableObject中的readonly去掉,用TypeScript的-readonly修饰符来移除属性的只读限制:
type DeepMutableObject<T> = { -readonly [P in keyof T]: DeepMutable<T[P]>; }
2. 优化类型实现的兼容性
我们可以把数组、Map、Set的判断逻辑调整为同时匹配普通和只读版本,并且用类型别名替代接口,避免接口带来的类型兼容问题:
优化后的DeepReadonly
type Primitive = string | number | boolean | null | undefined | symbol | bigint; type AnyFunction = (...args: any[]) => any; type DeepReadonly<T> = T extends AnyFunction | Primitive ? T : // 同时匹配普通数组和只读数组 T extends Array<infer R> | ReadonlyArray<infer R> ? readonly DeepReadonly<R>[] : // 同时匹配普通Map和只读Map T extends Map<infer K, infer V> | ReadonlyMap<infer K, infer V> ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>> : // 同时匹配普通Set和只读Set T extends Set<infer ItemType> | ReadonlySet<infer ItemType>? ReadonlySet<DeepReadonly<ItemType>>: T extends object ? { readonly [P in keyof T]: DeepReadonly<T[P]>; } : T;
优化后的DeepMutable
export type DeepMutable<T> = T extends AnyFunction | Primitive ? T : // 同时匹配普通数组和只读数组 T extends Array<infer R> | ReadonlyArray<infer R> ? DeepMutable<R>[] : // 同时匹配普通Map和只读Map T extends Map<infer K, infer V> | ReadonlyMap<infer K, infer V> ? Map<DeepMutable<K>, DeepMutable<V>> : // 同时匹配普通Set和只读Set T extends Set<infer ItemType> | ReadonlySet<infer ItemType>? Set<DeepMutable<ItemType>>: T extends object ? { -readonly [P in keyof T]: DeepMutable<T[P]>; } : T;
3. 在组件中正确使用转换后的类型
现在你可以移除@ts-ignore,并正确转换otherbuttonProps的类型:
public render() { const {handleClick, stl, otherbuttonProps, ...otherProps} = this.props; // 将只读的button props转为可变类型 const mutableButtonProps = otherbuttonProps as DeepMutable<typeof otherbuttonProps>; return ( <button type="button" className={css(getNewDeepMutable([style, stl]))} onMouseDown={this.handleClickW} onKeyDown={this.handleKeyDown} {...mutableButtonProps} {...otherProps} /> ) }
额外小贴士
如果你用的是TypeScript 4.5及以上版本,可以考虑使用内置的ReadonlyDeep类型(需要启用strictNullChecks和strictFunctionTypes编译选项),不过自定义的类型在复杂场景下会更灵活,适合你的组件需求。
内容的提问来源于stack exchange,提问作者Ben Carp

