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

时间:2017-12-18 11:51:44

标签: typescript generics

我知道在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<HTMLInputElement>('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>

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

1 个答案:

答案 0 :(得分:3)

是的,您不能在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>

我能想到的唯一解决方法是允许你使用&#34;泛型构造函数&#34;没有任何运行时开销就是将泛型参数放在类定义中;因为没有任何课程和#34;重载&#34;定义,你必须制作一个适用于所有情况的单一签名。这是一个例子:

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<E,N>类带有EN类型参数,其中N参数仅在构造函数中使用,然后只是后来被拖了。由于N具有默认值,因此您无需指定它(因此您只需编写Some<E>),因为类的结构类型并不依赖于{N 1}}您可以安全地将Some<E,N1>分配给Some<E,N2>,反之亦然。

新的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<CtorParams> Class<ClassParams>?)

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

更新

哦,哇,我有更好的解决方案。首先,将您的类重命名为其他名称,以便稍后使用真实姓名。在此,我已将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<E>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在发光时)

现在,看它工作:

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

你怎么看?