我在使用仿制药方面有困难;多态属性.我在做什么疯狂的事吗?

我已经把这段代码删减了很多.我有两个(或更多)扩展基类(TemplateContext)的类(WelcomeTemplateAddToAccountTemplate).每个都实现了自己的context形状.目标是当EmailService初始化时,它将接受模板的名称,typescript将知道给定模板需要什么上下文.

例如:

new EmailService("WelcomeTemplate", {
    signupUrl: 'https://test.com',
});

然而,在初始化模板的EmailService分钟内,我遇到了以下两个打字错误,这让我抓狂.我已经try 过几种方法,但仍会在这些错误上继续努力.

Argument of type 'AddToAccountContext | WelcomeContext' is not assignable to parameter of type 'AddToAccountContext & WelcomeContext'.

AddToAccountTemplate' is assignable to the constraint of type 'Template', but 'Template' could be instantiated with a different subtype of constraint 'AddToAccountTemplate | WelcomeTemplate'.

Naturally AddToAccountContext | WelcomeContextAddToAccountContext & WelcomeContext conflict. But I'm not certain why typescript is concluding & given I'm doing Templates[Key] which from generics which I believe should have the types for just at Key not a merged type for all Keys.

有没有更好的方法来组织这个场景中的子类?

这是TypeScript Playground美元.在那里摆弄可能更简单.

谢谢你和我一起看!


interface ITemplate<Context> {
  readonly context?: Context;
}
type TemplateContext = {};
class Template implements ITemplate<TemplateContext> {
  public context = {};
}
type AddToAccountContext = {
  loginUrl: string;
  accountName: string;
};
class AddToAccountTemplate extends Template {
  constructor(context: AddToAccountContext) {
    super();
    this.context = context;
  }

  public context: AddToAccountContext;
}
type WelcomeContext = {
  signupUrl: string;
};
class WelcomeTemplate extends Template {
  constructor(context: WelcomeContext) {
    super();
    this.context = context;
  }

  public context: WelcomeContext;
}
const templates = Object.freeze({
  WelcomeTemplate,
  AddToAccountTemplate,
});

type Templates = typeof templates;

class EmailService<
  Key extends keyof Templates,
  Template extends InstanceType<Templates[Key]>,
  Context extends Template["context"],
> {
  templateName: Key;
  context: Context;

  template: InstanceType<Templates[Key]>;

  constructor(templateName: Key, context: Context) {
    this.templateName = templateName;
    this.context = context;

    const TemplateClass = templates[this.templateName];
    this.template = new TemplateClass(this.context); // Error: Argument of type 'AddToAccountContext | WelcomeContext' is not assignable to parameter of type 'AddToAccountContext & WelcomeContext'.
  }
}

推荐答案

这里的问题是,编译器没有遵循类型TemplateClass、类型context和类型this.template之间的correlation.


为了便于讨论,我将把你的代码重构成一个只有一个generic类型参数K的版本,对应于你调用的Key.TemplateContext类型参数基本上只是Key上各种类型函数的别名,拥有额外的类型参数并不会使问题更容易讨论或解决:

type TemplateInstances =
  { [K in keyof typeof templates]: InstanceType<typeof templates[K]> };

type TemplateContexts =
  { [K in keyof TemplateInstances]: TemplateInstances[K]['context'] };

class EmailService<K extends keyof TemplateInstances> {
  templateName: K;
  context: TemplateContexts[K];

  template: TemplateInstances[K];

  constructor(templateName: K, context: TemplateContexts[K]) {
    this.templateName = templateName;
    this.context = context;

    const TemplateClass = templates[this.templateName];
    this.template = new TemplateClass(this.context); // error!
    //~~~~~~~~~~~ <-----------------> ~~~~~~~~~~~~~
  }
}

我已经将助手类型TemplateInstancesTemplateContexts定义为从templateName个字符串文本到相应实例类型和上下文类型的映射,因此稍后我们可以说TemplateInstances[K]代替Template类型参数,TemplateContexts[K]代替Context类型参数.

这个版本和你的版本有同样的问题:编译器抱怨被赋予了并集,而不是它想要的交集.但为什么呢?


TemplateClasscontextthis.template的类型取决于泛型类型参数K,泛型类型参数Kconstrainedunion type.因此,编译器认为它们各自都像并集一样.例如,它知道TemplateClass是两个可能的构造函数之一,context是适合这两个构造函数之一的输入.但它忽略了context肯定适合TemplateClass的事实.它担心TemplateClass可能是,比如说,AddToAccountTemplate构造函数,而context可能是WelcomeContext类型.编译器肯定会认为对TemplateClass安全的唯一输入是both个可能的构造函数肯定会接受的输入;这将是intersection种可能的输入类型.

长期以来,这种未能遵循相关性的做法一直是TypeScript的痛点之一,并促使我提交了microsoft/TypeScript#30581份文件.在TypeScript 4.6之前,这里唯一的方法是编写大量冗余代码,以使编译器相信类型安全性(无论如何,它不会与泛型一起工作),或者使用type assertions告诉编译器不要担心它无论如何都无法验证的类型安全性.

这样的类型断言是这里最简单的答案;在构造函数中,断言TemplateClass属于以下类型:

    const TemplateClass = templates[this.templateName] as
      new (c: TemplateContexts[K]) => TemplateInstances[K];
      
    this.template = new TemplateClass(this.context); // okay

现在没有错误了.实际上,你已经把确定templates[templateName]的类型的工作从编译器中移除了.现在一切都好了.是的,如果你把它改成完全坏掉的东西,比如

    const TemplateClass = templates[
      Math.random() < 0.5 ? "AddToAccountTemplate" : "WelcomeTemplate"
    ] as new (c: TemplateContexts[K]) => TemplateInstances[K]; // okay

因此,在进行类型断言时,需要注意一些.但这很简单.


TypeScript 4.6引入了大约improvements to generic indexed access种行为,专门用于以类型安全的方式处理相关的联合类型(好吧,相对而言,无论如何;请参见ms/TS#48730).

它包括将您的类型重构为mapped types个,并将其索引到以通用键类型显式表示的地方.这里的变化涉及annotating个变量,即templates个变量.这里有一种方法:

const _templates = {
  WelcomeTemplate,
  AddToAccountTemplate
};

type TemplateInstances =
  { [K in keyof typeof _templates]: InstanceType<typeof _templates[K]> };

type TemplateContexts =
  { [K in keyof TemplateInstances]: TemplateInstances[K]['context'] };

type TemplateConstructors =
  { readonly [K in keyof TemplateInstances]:
    new (c: TemplateContexts[K]) => TemplateInstances[K] };

const templates: TemplateConstructors = Object.freeze(_templates);

请注意,在上面的例子中,templates的值是相同的(为了便于打字,我使用了一个中间变量_templates),但templates的类型表示不同.它被显式地写为从泛型键K到构造函数类型new (c: TemplateContexts[K]) => TemplateInstances[K]的映射.

一旦我们这样做,EmailService的构造函数中的错误就会消失:

    this.templateName = templateName;
    this.context = context;
    const TemplateClass = templates[this.templateName];
    this.template = new TemplateClass(this.context); // okay!

好极了为了让大家明白,修复依赖于templates的映射类型表示,如果我们使用 struct 相同的普通对象类型,情况会再次发生变化:

const stillTemplates: {
  readonly WelcomeTemplate: new (x: WelcomeContext) => WelcomeTemplate;
  readonly AddToAccountTemplate: new (x: AddToAccountContext) => AddToAccountTemplate;
} = templates; // okay, it's a structrually identical type, but:

    // later
    const TemplateClass = stillTtemplates[this.templateName];
    this.template = new TemplateClass(this.context); // errors came back!
    //~~~~~~~~~~~ <-----------------> ~~~~~~~~~~~~

好了.编译器不够聪明,无法判断你所做的是安全的.要修复它,可以使用类型断言来处理和 suppress 错误,或者重构类型,为其提供所需的提示,以查看所做操作中的逻辑.

Playground link to code

Javascript相关问答推荐

VMWHTMLWebMKS-控制台连接良好,但没有屏幕且占用率高

假设我有2个对象,根据它们,我想要新对象

Webpack将懒惰加载的模块放入主块中

为什么(1 + Number.Min_UTE)等于1?

除了在Angular 16中使用快照之外,什么是可行且更灵活的替代方案?

用相器进行向内碰撞检测

仅圆角的甜甜圈图表

Redux工具包查询(RTKQ)端点无效并重新验证多次触发

django无法解析余数:[0] from carray[0]'

docx.js:如何在客户端使用文档修补程序

使用i18next在React中不重新加载翻译动态数据的问题

如何在mongoose中链接两个模型?

仅针对RTK查询中的未经授权的错误取消maxRetries

在Vite React库中添加子模块路径

单个HTML中的多个HTML文件

扩展类型的联合被解析为基类型

查询参数中的JAVASCRIPT/REACT中的括号

当我try 将值更改为True时,按钮不会锁定

在VS代码上一次设置多个变量格式

如何用javascript更改元素的宽度和高度?