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

Rap*_*ert 4 generics typescript

我知道在 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;
  }
}
Run Code Online (Sandbox Code Playgroud)

然后将正确键入它们的用法

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

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

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>
Run Code Online (Sandbox Code Playgroud)

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

new Some('input'); // Type is Some<Element> (the fallback) instead of Some<HTMLInputElement>
Run Code Online (Sandbox Code Playgroud)

(当然,使用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
Run Code Online (Sandbox Code Playgroud)

我知道我可以创建一个辅助函数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>
Run Code Online (Sandbox Code Playgroud)

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

jca*_*alz 9

是的,您不能在 constructor 函数上放置泛型参数,因为构造函数的类型参数会与类本身的任何可能的类型参数发生冲突

new Some<WAT>('input'); // Is WAT the class param or the constructor param?
Run Code Online (Sandbox Code Playgroud)

在 TypeScript 中肯定有一些地方让编译器理解你在做什么的唯一方法是添加运行时构造(用户定义的类型保护就是一个例子)。所以,你的辅助函数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);
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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;
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

这是有效的,因为Some<E,N>该类同时携带 theENtype 参数,其中该N参数仅在构造函数中使用,然后才被拖动。由于N具有默认值,因此您无需指定它(因此您只需编写Some<E>),并且由于类的结构类型不依赖于N您可以安全地将 a 分配Some<E,N1>给 a Some<E,N2>,反之亦然。

ElementTagNameMapWithDefault接口包含一个组合键'***default***'(您可能可以使用Symbol代替,但这只是一个概念验证),它允许指定E类型包含Element. 而且,由于N默认'***default***',默认值EElement

让我们确保它有效:

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

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

new Some('***default***'); // don't do this
Run Code Online (Sandbox Code Playgroud)

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

但在实践中不要这样做;这是丑陋和可怕的。静态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;
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

现在,定义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>
}
Run Code Online (Sandbox Code Playgroud)

最后,只需声明它_Some是 aSomeConstructor并为其命名Some

const Some: SomeConstructor = _Some;
Run Code Online (Sandbox Code Playgroud)

这是一个很小的运行时开销,我希望你觉得它可以接受(var Some = _Some在发射时)

现在,看它工作:

new Some(document.querySelector('option')!); // Some<HTMLOptionElement>
new Some(new Image()); // Some<HTMLImageElement>
new Some('input'); // Some<HTMLInputElement>
Run Code Online (Sandbox Code Playgroud)

你对那个怎么想的?