我有一个类,该类具有代表其 map 的已知元素的通用类型,如下所示:

abstract class Component { ... }

class Test<Known extends Component[]> {
    components: Map<string, Component> ...

    has<C extends Component>(comp: C): this is Test<[...Known, C]> {
       ...
    }
}

我希望在判断可用组件时可以使用Test.has()来更改其类型,但是,它会附加到类型而不是替换:

// test is Test<[AComp, BComp]>, so far so good
const test = new Test<[AComp, BComp]>();

const comp = new CComp();

if (sometest.has(comp)) {
    // now test is "Test<[AComp, BComp]> & Test<[AComp, BComp, CComp]>"
}

基本上,test.has()得到了正确的类型,但它不是替换它,而是"拥有"它.

我确实相信这符合TypScript引擎的预期,因为它不理解第二种类型实际上扩展了第一种类型,并认为它们应该被视为不同的事物.

我该如何告诉引擎第二个扩展了第一个,这可能吗?

使用可复制示例编辑:

abstract class Component {
}
class AComp extends Component {
  aValue = 1;
}
class BComp extends Component {
  bValue = 3;
}
type ClassOf<C extends Component> = new (...args: any[]) => C;
type TestGetResult<C extends Component, K extends Component[]> = C extends K[number] ? C : (C | undefined); 
class Test<K extends Component[]> {
  components = new Map<string, Component>();
  constructor(components: Component[]) {
    components.forEach(comp => this.components.set(comp.constructor.name, comp));
  }

  has<C extends Component>(comp: ClassOf<C>): this is Test<[...K, C]> {
    return [...this.components.keys()].includes(comp.name);
  }

  get<C extends Component>(comp: ClassOf<C>): TestGetResult<C, K> {
    return this.components.get(comp.name) as TestGetResult<C, K>; 
  }
}
// This class simulates us getting the Test instances from somewhere else in the code, their type is not immediately known when we return them but by
// using .has() we can infer some of it
class SomeStore {
  list: Test<any>[] = [];
  add(test: Test<any>) { this.list.push(test); }

  getTestWithSomeComponent<C extends Component>(comp: ClassOf<C>): Test<[C]> {
    for (let test of this.list) {
      if (test.has(comp)) {
        return test;
      }
    }
    throw new Error('No test found');
  }
}
///////////////////////////////////////////////////////////////////////////////////////
const store = new SomeStore();
const test1 = new Test<[AComp, BComp]>([new AComp(), new BComp()])
store.add(test1);                                                
                                                                
const testAfterGet = store.getTestWithSomeComponent(AComp);
if (testAfterGet.has(BComp)) {
  const a = testAfterGet.get(AComp);
  // Here is the issue
  const b = testAfterGet.get(BComp) // at this point, testAfterGet has the 'and' type
  console.log(a.aValue); 
  console.log(b.bValue); // b should not be undefined
}

推荐答案

只有当X的类型可分配给正在判断的对象的类型时,返回this is Xcustom type guard method才能正常工作. 也就是说,类型保护函数和方法只有narrow种类型,它们不会任意变异类型. 根据定义,intersection this & Xthis的缩小,因此该类型始终有效.

如果您不喜欢这样,那么您可以try 将Extract<X, this>the Extract utility type进行类似的操作,但如果X确实不能分配给this,那么Extract<X, this>可能会是never,并且这不会满足您的要求.

因为,在您的示例中,Test<[AComp, BComp]>不能分配给Test<[AComp]>:

declare const x: Test<[AComp, BComp]>;
const y: Test<[AComp]> = x; // error!
// Type 'Test<[AComp, BComp]>' is not assignable to type 'Test<[AComp]>'.

这无法直接按照要求工作.


交叉口通常应该做您想做的事情.但对于方法来说,它们确实会得到看起来像overload的结果.如果您不介意超载但想要更改它们的顺序,则可以显式地将返回类型注释为其他顺序中的交集(例如,X & this而不是this & X):

has<C extends Component>(comp: ClassOf<C>): this is Test<[...K, C]> & this {
    return [...this.components.keys()].includes(comp.name);
}

现在可以使用您的特定示例代码:

const testAfterGet = store.getTestWithSomeComponent(AComp);
if (testAfterGet.has(BComp)) {
    // const testAfterGet: Test<[AComp, BComp]> & Test<[AComp]>
    const a = testAfterGet.get(AComp);
    //    ^? const a: AComp
    const b = testAfterGet.get(BComp);
    //    ^? const b: BComp
    console.log(a.aValue);
    console.log(b.bValue); // b should not be undefined
}

但目前还不能100%确定这是否会符合您的整个代码库的预期.


理想情况下,当且仅当XY彼此之间存在某种明显的简单关系时,您会重构Test<X> extends Test<Y>.对于所写的代码来说,很难说.您似乎使用的是tuple types,它关心这些类型中的order,但我认为您实际上并不关心这一点?或者至少示例代码不会激励它(根据代码的 struct ,您如何区分Test<[AComp, BComp]>Test<[BComp, AComp]>之间的区别,特别是因为Test<K>的构造函数似乎根本不关心K? 我会认为这种重构超出了这个问题的范围.

Playground link to code

Typescript相关问答推荐

您可以创建一个类型来表示打字员吗

TypScript如何在 struct 上定义基元类型?

当类型断言函数返回假时,TypScript正在推断从不类型

如何通过TypeScript中的工厂函数将区分的联合映射到具体的类型(如类)?

在TypeScrip中分配回调时,参数的分配方向与回调本身相反.为什么会这样呢?

带占位符的模板文字类型何时扩展另一个?

如何根据特定操作隐藏按钮

已解决:如何使用值类型限制泛型键?

为什么ESLint会抱怨函数调用返回的隐式`any`?

将带点的对象键重新映射为具有保留值类型的嵌套对象

将对象的属性转换为正确类型和位置的函数参数

创建依赖函数参数的类型

如何避免从引用<;字符串&>到引用<;字符串|未定义&>;的不安全赋值?

是否将Angular *ngFor和*ngIf迁移到新的v17语法?

TypeScrip:如何缩小具有联合类型属性的对象

使用来自API调用的JSON数据的Angular

TaskListProps[]类型不可分配给TaskListProps类型

使用Cypress测试从Reaction受控列表中删除项目

如何正确键入泛型相对记录值类型?

包含多个不同类构造函数的接口的正确类型?