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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 11:58:11