TypeScript中限制类型继承object失效:字符串被错误允许
问题描述
我尝试为包装类中包含的attributes对象强制执行类型约束,目的是让getOne/setOne方法在设置或获取属性时具备正确的类型提示。但目前实现存在问题:AttributeCollection的构造函数竟能接受字符串作为参数,这不符合类的设计初衷。
代码示例
type AttributeMap<M extends object> = { [key in keyof M]: M[key]; }; export class AttributeCollection<M extends object> { constructor(private attributes: AttributeMap<M>) {} public getOne<K extends keyof AttributeMap<M>>(key: K): M[K] { return this.attributes[key]; } public setOne<K extends keyof AttributeMap<M>>(key: K, value: M[K]): void { this.attributes[key] = value; } } const acCorrectImplicit = new AttributeCollection({ a: 'str'}); // ok const acCorrectExplicit = new AttributeCollection<{ a: string }>({ a: 'str'}); // ok const acWrong = new AttributeCollection('str'); // ok, but shouldn't be /* 得到的类型: const acWrong: AttributeCollection<{ toString: () => string; charAt: (pos: number) => string; charCodeAt: (index: number) => number; concat: (...strings: string[]) => string; indexOf: (searchString: string, position?: number | undefined) => number; ... 37 more ...; [Symbol.iterator]: () => IterableIterator<...>; }> */ const am: AttributeMap<'str'> = 'str'; // error console.log(acCorrectImplicit, acCorrectExplicit, acWrong, am);
问题原因
问题出在TypeScript的类型推断机制:当你传入原始类型(如字符串)给泛型约束为M extends object的类构造函数时,TypeScript会自动将M推断为对应的包装对象类型(比如String),因为原始类型本身不满足extends object,但包装对象类型满足。同时,原始类型的值可以赋值给对应的包装对象类型(TypeScript允许原始类型与包装对象之间的兼容赋值),因此构造函数会接受字符串参数。
另外,你定义的AttributeMap<M>本质上是M的类型别名,没有任何实际的类型转换作用,因此无法阻止这种推断行为。
解决方案
我们可以通过在构造函数参数上添加条件类型约束,排除掉原始类型的包装对象类型,从而阻止构造函数接受原始类型参数:
export class AttributeCollection<M extends object> { // 添加条件类型,排除String/Number/Boolean/Symbol/BigInt包装对象 constructor( private attributes: M extends String | Number | Boolean | Symbol | BigInt ? never : M ) {} public getOne<K extends keyof M>(key: K): M[K] { return this.attributes[key]; } public setOne<K extends keyof M>(key: K, value: M[K]): void { this.attributes[key] = value; } } // 测试: const acCorrectImplicit = new AttributeCollection({ a: 'str'}); // ok const acCorrectExplicit = new AttributeCollection<{ a: string }>({ a: 'str'}); // ok const acWrong = new AttributeCollection('str'); // 报错:Argument of type 'string' is not assignable to parameter of type 'never'.
另外,你可以去掉多余的AttributeMap类型,因为它并没有改变原类型的结构,直接使用M即可简化代码。
内容的提问来源于stack exchange,提问作者Ivan Yarych
相关产品推荐
相关产品推荐

