简体   繁体   English

声明构造函数以从TypeScript中的(keyof)参数正确推断泛型

[英]Declare a constructor to correctly infer generic type from (keyof) argument in TypeScript

I know that in TypeScript I can declare functions like these: 我知道在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;
  }
}

Their usage will then be correctly typed 然后将正确键入其用法

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

How can I achieve the same thing with constructors of generic classes? 我如何用泛型类的构造函数实现相同的目的?

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>

But I can't seem to get the following to work: 但我似乎无法使以下工作正常进行:

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

(Of course, using new Some<HTMLInputElement>('input') works but I should not have to explicitly type this if I already have the ElementTagNameMap doing this for me.) (当然,使用new Some<HTMLInputElement>('input')可以,但是如果我已经有ElementTagNameMap为我执行此操作,则不必显式键入此名称。)

I have tried adding overloads to the constructor, like I did to the function in the previous example: 我尝试过向构造函数添加重载,就像在上一个示例中对函数所做的那样:

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

I know I could create a helper function createSome : 我知道我可以创建一个辅助函数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>

But isn't there a way to achieve this directly? 但是没有办法直接实现这一目标吗? It seems counter-intuitive that I need to add a run-time construct (helper function) to get a specific compile-time behaviour (type inference). 似乎有悖常理,我需要添加运行时构造(帮助程序函数)以获取特定的编译时行为(类型推断)。

Yes, you can't put a generic parameter on the constructor function because the type parameter for the constructor would collide with any possible type parameter for the class itself: 是的,您不能在constructor函数上放置通用参数,因为constructor函数的类型参数会与类本身的任何可能的类型参数发生冲突

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

There are definitely places in TypeScript where the only way to get the compiler to understand what you're doing is to add runtime constructs ( user-defined type guards are an example of this). 在TypeScript中肯定有很多地方,让编译器了解您正在执行的操作的唯一方法是添加运行时构造( 用户定义的类型防护是此示例)。 So, your helper function createSome() is reasonable. 因此,您的辅助函数createSome()是合理的。 If you make the constructor private and include createSome() as a static method on the class, you've exposed only the behavior you want and it is still packaged fairly nicely: 如果将构造函数设为私有,并将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);
  }
}

It isn't even that unwieldy for users: 对于用户来说,这还不是那么麻烦:

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

The only workaround I can think of that allows you to use a "generic constructor" without any runtime overhead would be to put the generic parameters in the class definition instead; 我能想到的唯一允许您使用“通用构造函数”而没有任何运行时间开销的解决方法是,将通用参数放入类定义中; and since there aren't any class "overload" definitions, you have to make a single signature that works for all cases. 并且由于没有任何类“重载”定义,因此您必须制作一个适用于所有情况的单个签名。 Here's an example: 这是一个例子:

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;
    }
  }
}

This works because the Some<E,N> class carries around both the E and the N type parameters, where the N parameter is only ever used in the constructor and then just gets dragged around later. 这之所以行得通,是因为Some<E,N>类同时包含EN类型参数,其中N参数仅在构造函数中使用过,然后在以后拖动。 Since N has a default value you don't need to specify it (so you can just write Some<E> ), and since the structural type of the class doesn't depend on N you can safely assign a Some<E,N1> to a Some<E,N2> and vice versa. 由于N具有默认值,因此您无需指定它(因此您只需编写Some<E> ),并且由于该类的结构类型不依赖于N您可以安全地分配Some<E,N1>转换为Some<E,N2> ,反之亦然。

The new ElementTagNameMapWithDefault interface includes a made-up key '***default***' (you can probably use a Symbol instead, but this is just a proof-of-concept) that allows the specified E type to include Element . 新的ElementTagNameMapWithDefault接口包括一个组成键'***default***' (您可以改用Symbol ,但这只是概念证明),它允许指定的E类型包含Element And since N defaults to '***default***' , the default value of E will be Element . 并且由于N默认为'***default***' ,所以E的默认值为Element

Let's make sure it works: 让我们确保它起作用:

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

All good. 都好。 Of course it allows this nonsense: 当然,它允许这种废话:

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

which is why you'd probably want a private Symbol if you did this in practice. 因此,如果您在实践中这样做,可能会想要一个私有符号。

But don't do this in practice; 但是不要在实践中这样做; it's ugly and terrible. 丑陋而可怕。 The static create() method is probably the least messy solution in TypeScript. 静态create()方法可能是TypeScript中最混乱的解决方案。


If you really want generic constructors, you might head over to Github and try to reopen Microsoft/TypeScript#10860 or make a new issue that references it. 如果您真的想要通用构造函数,则可以转到Github,然后尝试重新打开Microsoft / TypeScript#10860或发出引用它的新问题。 Perhaps a proposal about how to deal with distinguishing constructor type parameters from class parameters would be needed? 也许需要有关如何处理区分构造函数类型参数和类参数的建议? ( new<CtorParams> Class<ClassParams> ?) new<CtorParams> Class<ClassParams> ?)

Anyway, hope that helps; 无论如何,希望能有所帮助; good luck! 祝好运!


UPDATE UPDATE

Oh, wow, I have a better solution for you. 哦,哇,我为您提供了更好的解决方案。 First, rename your class to something else so you can use the real name later. 首先,将您的班级重命名为其他名称,以便以后使用真实姓名。 Here, I've changed Some to _Some : 在这里,我将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;
    }
  }
}

Now, define the Some<E> and SomeConstructor interfaces specifying the types exactly as you want. 现在,定义Some<E>SomeConstructor接口,以完全根据需要指定类型。 Note how in this case the constructor can indeed be overloaded and generified: 请注意,在这种情况下,构造函数的确可以重载和泛化:

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>
}

Finally, just declare that _Some is a SomeConstructor and give it the name Some : 最后,只需声明_SomeSomeConstructor并将其命名为Some

const Some: SomeConstructor = _Some;

This is such a tiny amount of runtime overhead that I hope you find it acceptable ( var Some = _Some at emit) 这是一个很小的运行时开销,希望您能接受( var Some = _Some当发)

Now, watch it work: 现在,观看它的工作情况:

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

What do you think of that? 你对那个怎么想的?

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM