我试图缩小.forEach循环中的类型(下面代码片段中的最后一条语句).

在这个循环中,TS认为objObjA | ObjB(因为TS只是将数组中该键的所有可能值合并在一起),而方法需要ObjA & ObjB(因为TS列出了一系列可能的参数,并将它们交集),这显然是不一样的.

但是,代码显然是正确的(因为数组元素的objcls键是匹配的,可以一起使用):

type ObjBase = { id: string };

abstract class Base<T extends ObjBase> {
  abstract processObj(obj: T): string;

  useObj(obj: T) {
    return this.processObj(obj);
  }
}

// Module A
type ObjA = { someKey: string } & ObjBase;

class DerivedA extends Base<ObjA> {
  processObj(obj: ObjA): string {
      return obj.id + obj.someKey;
  }
}
// ========

// Module B
type ObjB = { otherKey: string } & ObjBase;

class DerivedB extends Base<ObjB> {
  processObj(obj: ObjB): string {
      return obj.id + obj.otherKey;
  }
}
// ========

const modules = [
  { obj: { id: 'a', someKey: '' } as ObjA, cls: new DerivedA() },
  { obj: { id: 'b', otherKey: '' } as ObjB, cls: new DerivedB() },
] as const;

modules.forEach(module => {
  module.cls.processObj(module.obj);
});

Playground

How do I fix the error inside that 100 and make TS understand that it's safe?

所有这一切背后的意图是在应用程序中有几个‘模块’,它们重复使用大量代码(这里用Base#useObjforEach循环的内部表示).一百零二

Note that I do not want to refer to each individual module entities inside that loop as it would defeat the purpose of making them conform to one interface.

推荐答案

本质上,modules应该是heterogeneous array,其中每个元素不是某个固定的Module类型,而是每个元素是不同的generic类型,如[GenericModule<Obj1>, GenericModule<Obj2>, ⋯],其中GenericModule<T>

interface GenericModule<T extends ObjBase> {
  obj: T,
  cls: Base<T>
}

而在你的forEach()循环中,你真的只想处理GenericModule<T>,some,T,你并不关心.这是existentially quantified generic types的典型用例之一(例如"对于modulesthere exists的每个元素,都有T个元素,使得该元素是GenericModule<T>类型的).但是,像大多数其他带有泛型的语言一样,TypeScrip不直接支持存在泛型.(尽管microsoft/TypeScript#14466有一个长期开放的特性请求.)所以您需要一种不同的方法.

您可以采取不同的方法:


如果您想要支持一些固定的已知T集合,您可以用union替换存在泛型.(存在主义泛型可以被认为是"无限联合",而常规泛型或"通用"泛型可以被认为是"无限intersections".)但在forEach()内部,类型脚本将不能将其视为安全的.即使联合GenericModule<Obj1> | GenericModule<Obj2>应该允许您安全地编写module.cls.processObj(module.obj),编译器仍抱怨.它看不到module.clsmodule.obj之间的correlation.相反,module.cls.processObj成为函数的联合,并且您只能安全地调用具有交集参数的函数(请参见the release notes for TS3.3),并且您将得到大约ObjA & ObjB的错误.

相关unions 的这个问题是microsoft/TypeScript#30581次会议的主题.microsoft/TypeScript#47109中描述了推荐/支持的方法,它涉及使用"基本"映射接口将联合重构为特殊形式,在该接口中为联合的每个元素指定一个键,例如

interface Mapping {
  a: ObjA,
  b: ObjB
}

然后,您的所有操作都以该接口的形式编写,或者以该接口的通用indexes into编写,或者通过该接口写入mapped types.就像这样:

type Module<K extends keyof Mapping = keyof Mapping> =
  { [P in K]: GenericModule<Mapping[P]> }[K]

const modules: Module[] = [
  { obj: { id: 'a', someKey: '' } as ObjA, cls: new DerivedA() },
  { obj: { id: 'b', otherKey: '' } as ObjB, cls: new DerivedB() },
] as const;

modules.forEach(<K extends keyof Mapping>({ obj, cls }: Module<K>) => {
  cls.processObj(obj); // okay
});

Module<K>类型接受K的泛型类型参数,它是keyof Mapping的某个子集.Module<"a">相当于GenericModule<ObjA>,Module<"b">相当于GenericModule<ObjB>,Module就相当于default Module<"a" | "b">,相当于和GenericModule<ObjA> | GenericModule<B>.所以modulesModule[]类型的.

现在forEach()回调是通用的,接受Module<K>. 这是可行的,因为类型cls.process被视为单一的泛型函数类型(obj: Mapping[K]) => string,而obj被视为Mapping[K].不再涉及联合,只是使用预期的参数类型调用单个泛型函数类型.


另一种方法,如果你没有一个固定的类型列表来支持,或者你不想给这些类型分配键,那就是通过将它们转换为类似Promise的 struct 来模拟存在泛型. 使用存在泛型相当于提供通用泛型,反之亦然.这在microsoft/TypeScript#14466中的this comment处有描述.

给定GenericModule<T>,我们可以定义Module,即存在主义版本,如下所示:

type Module = <R>(cb: <T extends ObjBase>(module: GenericModule<T>) => R) => R;

因此,Module就像是GenericModule<T>的promise ,而你不知道T.您所能做的就是给它一个类似then的回调,该回调返回您do知道其类型的内容.对于任何普通的GenericModule,你可以把它包装成一个Module:

const someModule = <T extends ObjBase>(module: GenericModule<T>): Module => cb => cb(module);

然后,您的modules数组可以重新包装:

const someModules: Module[] =
  [someModule(modules[0]), someModule(modules[1])];

现在你可以通过给它们每个回调来处理它们:

someModules.forEach(m => m(({ obj, cls }) => {
  cls.processObj(obj); // okay
}));

这些方法中的每一种都保留了一定程度的类型安全,而且每种方法都有优点和缺点,而且每一种方法都不是没有一些努力和复杂性.对于特定的任务,哪一个是可接受的,如果有的话,取决于用例和需求.

Playground link to code

Typescript相关问答推荐

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

如何访问Content UI的DatePicker/中的sx props of year picker?'<>

返回同时具有固定键和变量键的对象的函数的返回类型

创建一个根据布尔参数返回类型的异步函数

泛型函数中输入参数类型的推断问题

打字脚本中泛型和直接类型重新映射的不同行为

隐式键入脚本键映射使用

如何在处理数组时缩小几个派生类实例之间的类型范围?

部分更新类型的类型相等

Vue3 Uncaught SyntaxError:每当我重新加载页面时,浏览器控制台中会出现无效或意外的令牌

创建依赖函数参数的类型

对有效枚举值的常量字符串进行常规判断

棱角分明-什么是...接口类型<;T>;扩展函数{new(...args:any[]):t;}...卑鄙?

顶点堆叠的图表条形图未在正确的x轴上显示

内联类型断言的工作原理类似于TypeScrip中的断言函数?

打字:注解`是字符串`而不是`布尔`?

Typescript不允许在数组中使用联合类型

映射类型不是数组类型

如何知道使用TypeScript时是否在临时工作流内运行

基于参数的 TS 返回类型