【问题标题】:Declare a constructor to correctly infer generic type from (keyof) argument in TypeScript声明一个构造函数以从 TypeScript 中的 (keyof) 参数正确推断泛型类型
【发布时间】:2017-12-18 11:51:44
【问题描述】:

我知道在 TypeScript 中我可以声明如下函数:

function doSomething<E extends Element>(el : E) : E;
function doSomething<N extends keyof ElementTagNameMap>(selector : N) : ElementTagNameMap[N];
function doSomething<E extends Element>(el : E | keyof ElementTagNameMap) {
  if(typeof el === 'string') {
    return document.createElement(el) as Element as E;
  } else {
    return el;
  }
}

它们的用法将被正确输入

doSomething(document.querySelector('option')!) // return value typed as HTMLOptionElement
doSomething(new Image()); // return value typed as HTMLImageElement
doSomething('input'); // return value typed as HTMLInputElement

如何使用泛型类的构造函数实现相同的功能?

class Some<E extends Element> {
  public element : E;

  constructor(el : E | keyof ElementTagNameMap) {
    if(typeof el === 'string') {
      this.element = document.createElement(el) as Element as E;
    } else {
      this.element = el;
    }
  }
}


new Some(document.querySelector('option')!); // Works, type is Some<HTMLOptionElement>
new Some(new Image()); // Works, type is Some<HTMLImageElement>

但我似乎无法让以下工作:

new Some('input'); // Type is Some<Element> (the fallback) instead of Some<HTMLInputElement>

(当然,使用 new Some&lt;HTMLInputElement&gt;('input') 可以,但如果我已经有 ElementTagNameMap 为我执行此操作,我不应该显式输入。)

我已经尝试向构造函数添加重载,就像我在上一个示例中对函数所做的那样:

constructor<N extends keyof ElementTagNameMap>(el : N) : Some<ElementTagNameMap[N]>;
// ⇒ Error: Type parameters cannot appear on a constructor function
constructor<N extends keyof ElementTagNameMap>(this : Some<ElementTagNameMap[N]>, el : N);
// ⇒ Error: A constructor cannot have a `this` parameter

我知道我可以创建一个辅助函数createSome

function createSome<E extends Element>(el : E) : Some<E>;
function createSome<N extends keyof ElementTagNameMap>(selector : N) : Some<ElementTagNameMap[N]>;
function createSome<E extends Element>(el : E | keyof ElementTagNameMap) {
  return new Some(el);
}

createSome(document.querySelector('option')!); // Works: type is Some<HTMLOptionElement>
createSome(new Image()); // Works: type is Some<HTMLImageElement>
createSome('input'); // Works too now: type is Some<HTMLInputElement>

但是没有办法直接实现这一点吗?我需要添加一个运行时构造(辅助函数)来获得特定的编译时行为(类型推断),这似乎违反直觉。

【问题讨论】:

  • 能否详细说明您的问题?我是否理解正确,您不喜欢代码:new Some(new Image()); 创建 Some&lt;Element&gt; 类型的实例,而不是 new Some&lt;HTMLImageElement&gt; 的事实?
  • @MarkDolbyrev 是的。或者更具体地说:如何向构造函数添加重载,其工作方式与我给出的函数示例相同。
  • @MarkDolbyrev 我误读了您的评论:实际上,new Some(new Image()) 的类型符合预期,我想正确推断出new Some('img')(通过keyof ElementTagNameMap)。
  • 我用您可能更喜欢的东西更新了我的解决方案;看看吧。
  • @jcalz 太棒了。我希望我能再次投票……

标签: typescript generics


【解决方案1】:

是的,您不能在 constructor 函数上放置泛型参数,因为构造函数的类型参数将 collide 与类本身的任何可能的类型参数:

new Some<WAT>('input'); // Is WAT the class param or the constructor param?

在 TypeScript 中肯定有一些地方,让编译器了解你在做什么的唯一方法是添加运行时构造(user-defined type guards 就是一个例子)。所以,你的辅助函数createSome() 是合理的。如果您将构造函数设为私有并将createSome() 作为静态方法包含在类中,则您只公开了您想要的行为,并且它仍然被很好地打包:

class Some<E extends Element> {
  public element: E;

  private constructor(el: E | keyof ElementTagNameMap) {
    if (typeof el === 'string') {
      this.element = document.createElement(el) as Element as E;
    } else {
      this.element = el;
    }
  }

  static create<E extends Element>(el: E): Some<E>;
  static create<N extends keyof ElementTagNameMap>(selector: N): Some<ElementTagNameMap[N]>;
  static create<E extends Element>(el: E | keyof ElementTagNameMap) {
    return new Some(el);
  }
}

它对用户来说甚至不是那么笨拙:

// not so bad
Some.create(document.querySelector('option')!); // Some<HTMLOptionElement>
Some.create(new Image()); // Some<HTMLImageElement>
Some.create('input'); // Some<HTMLInputElement>

我能想到的唯一允许您使用“通用构造函数”而没有任何运行时开销的解决方法是将通用参数放在类定义中;并且由于没有任何类“重载”定义,因此您必须创建一个适用于所有情况的签名。这是一个例子:

interface ElementTagNameMapWithDefault extends ElementTagNameMap {
  '***default***': Element;
}

class Some<E extends ElementTagNameMapWithDefault[N], N extends keyof ElementTagNameMapWithDefault = '***default***'> {
  public element: E;

  constructor(el: N);
  constructor(el: E);
  constructor(el: E | N) {
    if (typeof el === 'string') {
      this.element = document.createElement(el) as E;
    } else {
      this.element = el;
    }
  }
}

之所以有效,是因为Some&lt;E,N&gt; 类同时带有EN 类型参数,其中N 参数只在构造函数中使用,然后才被拖来拖去。因为N 有一个默认值,所以你不需要指定它(所以你可以写Some&lt;E&gt;),并且由于类的结构类型不依赖于N,你可以安全地分配一个@987654340 @ 到 Some&lt;E,N2&gt;,反之亦然。

新的ElementTagNameMapWithDefault 接口包含一个虚构的密钥'***default***'(您可能可以改用Symbol,但这只是一个概念验证),它允许指定的E 类型包含Element。由于N 默认为'***default***'E 的默认值为Element

让我们确保它有效:

new Some(document.querySelector('option')!); // Some<HTMLOptionElement, "***default***">
new Some(new Image()); // Some<HTMLImageElement, "***default***">
new Some('input'); // Some<HTMLInputElement, "input">

一切都好。当然它允许这种废话:

new Some('***default***'); // don't do this

这就是为什么如果您在实践中这样做,您可能需要一个私有符号。

但在实践中不要这样做;这是丑陋和可怕的。静态 create() 方法可能是 TypeScript 中最不混乱的解决方案。


如果你真的想要泛型构造函数,你可以前往Github 并尝试重新打开Microsoft/TypeScript#10860 或创建一个引用它的新问题。也许需要关于如何处理区分构造函数类型参数和类参数的建议? (new&lt;CtorParams&gt; Class&lt;ClassParams&gt;?)

无论如何,希望对您有所帮助;祝你好运!


更新

哦,哇,我有一个更好的解决方案给你。首先,将您的班级重命名为其他名称,以便您以后可以使用真实姓名。在这里,我将Some 更改为_Some

class _Some<E extends Element> {
  public element: E;

  constructor(el: E | keyof ElementTagNameMap) {
    if (typeof el === 'string') {
      this.element = document.createElement(el) as Element as E;
    } else {
      this.element = el;
    }
  }
}

现在,定义 Some&lt;E&gt;SomeConstructor 接口,完全按照您的需要指定类型。请注意,在这种情况下,构造函数确实可以被重载和泛化:

interface Some<E extends Element> extends _Some<E> {

}

interface SomeConstructor {
  new <N extends keyof ElementTagNameMap>(el: N): Some<ElementTagNameMap[N]>
  new <E extends Element>(el: E): Some<E>
}

最后,只需声明 _SomeSomeConstructor 并将其命名为 Some

const Some: SomeConstructor = _Some;

这是非常小的运行时开销,我希望你能接受(var Some = _Some at emit)

现在,看它工作:

new Some(document.querySelector('option')!); // Some<HTMLOptionElement>
new Some(new Image()); // Some<HTMLImageElement>
new Some('input'); // Some<HTMLInputElement>

你怎么看?

【讨论】:

  • 用户定义的类型保护是一个运行时特性,因为它们断言只能在运行时断言的东西,而 ElementTagNameMap 完全在编译时确定,所以我不认为这是一个恰当的比较……
猜你喜欢
  • 1970-01-01
  • 2017-06-02
  • 2016-11-01
  • 2019-09-26
  • 2022-01-26
  • 2020-03-11
  • 2016-12-05
  • 1970-01-01
  • 2021-08-06
相关资源
最近更新 更多