Typescript: 静态成员的多态“this”

创建于 2015-12-01  ·  150评论  ·  资料来源: microsoft/TypeScript

在尝试实现一个相当基本但多态的活动记录样式模型系统时,当与构造函数或模板/泛型结合使用时,我们会遇到类型系统不尊重this的问题。

我之前在这里发布过关于此的内容,#5493,#5492 似乎也提到了这种行为。

这是我制作的一个 SO 帖子:
http://stackoverflow.com/questions/33443793/create-a-generic-factory-in-typescript-unsolved

我已将 #5493 中的示例回收到这张票中以供进一步讨论。 我想要一张公开的票,代表对这种事情和讨论的渴望,但另外两张是关闭的。

这是一个概述模型Factory的示例,该模型生成模型。 如果您想自定义从BaseModel Factory ,您应该能够覆盖它。 但是这失败了,因为this不能在静态成员中使用。

// Typically in a library
export interface InstanceConstructor<T extends BaseModel> {
    new(fac: Factory<T>): T;
}

export class Factory<T extends BaseModel> {
    constructor(private cls: InstanceConstructor<T>) {}

    get() {
        return new this.cls(this);
    }
}

export class BaseModel {
    // NOTE: Does not work....
    constructor(private fac: Factory<this>) {}

    refresh() {
        // get returns a new instance, but it should be of
        // type Model, not BaseModel.
        return this.fac.get();
    }
}

// Application Code
export class Model extends BaseModel {
    do() {
        return true;
    }
}

// Kinda sucks that Factory cannot infer the "Model" type
let f = new Factory<Model>(Model);
let a = f.get();

// b is inferred as any here.
let b = a.refresh();

也许这个问题很愚蠢,并且有一个简单的解决方法。 我欢迎就如何实现这种模式发表评论。

In Discussion Suggestion

最有用的评论

关闭此问题是否有原因?

在我看来,多态 this 在静态上不起作用的事实基本上使这个功能成为 DOA。 迄今为止,我从来没有真正需要实例成员上的多态 this,但我需要每隔几周处理一次静态,因为处理静态的系统在早期就已经完成了。 当这个特性被宣布时,我喜出望外,随后当意识到它只适用于实例成员时,我感到失望。

用例非常基本且非常常见。 考虑一个简单的工厂方法:

class Animal
{
    static create(): this
    {
        return new this();
    }
}

class Bunny extends Animal
{
    hop()
    {
    }
}

Bunny.create().hop() // Type error!! Come on!!

在这一点上,我要么诉诸丑陋的铸造,要么在每个继承者中乱扔static create()方法。 没有这个特性似乎是语言中一个相当大的完整性漏洞。

所有150条评论

我的船的大小和形状非常相似! 哎哟!

超类中的工厂方法返回其子类的新实例。 我的代码的功能有效,但需要我转换返回类型:

class Parent {
    public static deserialize(data: Object): any { ... create new instance ... }
    // Can't return a this type from statics! ^^^ :(
}

class Child extends Parent { ... }

let data = { ... };
let aChild: Child = Child.deserialize(data);
//           ^^^ Requires a cast as type cannot be inferred.

我今天也遇到了这个问题!

一个fixup的解决方案是将子类型作为泛型传入基类,这是我暂时申请的解决方案:

class Parent {
    static create<T extends Parent>(): T {
        let t = new this();

        return <T>t;
    }
}

class Child extends Parent {
    field: string;
}

let b = Child.create<Child>();

关闭此问题是否有原因?

在我看来,多态 this 在静态上不起作用的事实基本上使这个功能成为 DOA。 迄今为止,我从来没有真正需要实例成员上的多态 this,但我需要每隔几周处理一次静态,因为处理静态的系统在早期就已经完成了。 当这个特性被宣布时,我喜出望外,随后当意识到它只适用于实例成员时,我感到失望。

用例非常基本且非常常见。 考虑一个简单的工厂方法:

class Animal
{
    static create(): this
    {
        return new this();
    }
}

class Bunny extends Animal
{
    hop()
    {
    }
}

Bunny.create().hop() // Type error!! Come on!!

在这一点上,我要么诉诸丑陋的铸造,要么在每个继承者中乱扔static create()方法。 没有这个特性似乎是语言中一个相当大的完整性漏洞。

@paul-go 问题没有解决……?

@paul-go 我也对这个问题感到沮丧,但以下是我发现的最可靠的解决方法。 每个 Animal 子类都需要调用 super.create() 并将结果转换为它的类型。 没什么大不了的,它是一个可以轻松移除的单衬里。

编译器、智能感知,最重要的是兔子都很高兴。

class Animal {
    public static create<T extends Animal>(): T {
        let TClass = this.constructor.prototype;
        return <T>( new TClass() );
    }
}

class Bunny extends Animal {    
    public static create(): Bunny {
        return <Bunny>super.create();
    }

    public hop(): void {
        console.log(" Hoppp!! :) ");
    }
}

Bunny.create().hop();

         \\
          \\_ " See? I am now a happy Bunny! "
           (')   " Don't be so hostile! "
          / )=           " :P "
        o( )_


@RyanCavanaugh哎呀……出于某种原因,我将此与#5862 混淆了……抱歉战斧侵略:-)

@ Think7是的……因此“在每个继承者中都采用丑陋的铸造或乱扔静态 create() 方法”。 但是,当您是库开发人员并且您不能真正强制最终用户在他们从您那里继承的类中实现一堆类型化的静态方法时,这非常困难。

法律。 完全错过了您的代码下的所有内容:D

嗯,值得,要画一只兔子。

:+1: 兔子

:兔子: :心:

+1,肯定想看这个

有没有关于这个话题的讨论更新?

它仍然在我们巨大的建议积压中。

Javascript 已经以这种模式正确运行。 如果 TS 也可以遵循,那将使我们免于大量样板/额外代码。 “模型模式”是一个非常标准的模式,我希望 TS 能像 JS 那样工作。

出于与其他所有人相同的“CRUD 模型”原因,我也非常喜欢此功能。 我在静态方法上比实例方法更需要它。

这将为#8164 中描述的问题提供一个简洁的解决方案。

有覆盖和泛型的“解决方案”很好,但它们并没有真正解决这里的任何问题 - 拥有此功能的全部目的是避免此类覆盖/强制转换并与this的返回方式保持一致类型在实例方法中处理。

我正在研究 Sequelize 4.0 的类型,它使用一种方法,您可以将Model类作为子类。 该模型类有无数静态方法,如findById()等,当然不会返回Model而是您的子类,即静态上下文中的this

abstract class Model {
    public static tableName: string;
    public static findById(id: number): this { // error: a this type is only available in a non-static member of a class or interface 
        const rows = db.query(`SELECT * FROM ${this.tableName} WHERE id = ?`, [id]);
        const instance = new this();
        for (const column of Object.keys(rows[0])) {
            instance[column] = rows[0][column];
        }
        return instance;        
    }
}

class User extends Model {
    public static tableName = 'users';
    public username: string;    
}

const user = User.findById(1); // user instanceof User

目前无法输入。 Sequelize 是 Node 的_the_ ORM,很遗憾无法输入。 真的很需要这个功能。 唯一的方法是每次调用这些函数之一或覆盖它们中的每一个时进行强制转换,调整返回类型并且除了调用super.method()之外什么都不做。

同样相关的是静态成员不能引用泛型类型参数 - 一些方法采用可以通过类型参数键入的模型属性的对象文字,但它们仅可用于实例成员。

😰 不敢相信这仍然没有修复/添加.....

我们可以很好地利用这一点:

declare class NSObject {
    init(): this;
    static alloc(): this;
}

declare class UIButton extends NSObject {
}

let btn: UIButton = UIButton.alloc().init();

这是一个我希望工作的用例(从 https://github.com/Microsoft/TypeScript/issues/9775 迁移而来,我赞成这样做)

目前,构造函数的参数不能在其中使用this类型:

class C<T> {
    constructor(
        public transformParam: (self: this) => T // not works
    ){
    }
    public transformMethod(self: this) : T { // works
        return undefined;
    }
}

预期:这可用于构造函数参数。

一个寻求解决的问题:

  • 能够以我的 fluent API 为基础,或者围绕另一个 fluent API 重用相同的代码:
class TheirFluentApi {
    totallyUnrelated(): TheirFluentApi {
        return this;
    }
}

class MyFluentApi<FluentApi> {
    constructor(
        public toNextApi: (self: this) => FluentApi // let's imagine it works
    ){
    }
    one(): FluentApi {
        return this.toNextApi(this);
    }
    another(): FluentApi {
        return this.toNextApi(this);
    }
}

// self based fluent API;
const selfBased = new MyFluentApi(this => this);
selfBased.one().another();

// foreign based fluent API:
const foreignBased = new MyFluentApi(this => new TheirFluentApi());
foreignBased.one().totallyUnrelated();

面向未来的解决方法:

class Foo {

    foo() { }

    static create<T extends Foo>(): Foo & T {
        return new this() as Foo & T;
    }
}

class Bar extends Foo {

    bar() {}
}

class Baz extends Bar {

    baz() {}
}

Baz.create<Baz>().foo()
Baz.create<Baz>().bar()
Baz.create<Baz>().baz()

这样,当 TypeScript 支持静态成员this时,新用户代码将是Baz.create()而不是Baz.create<Baz>() ,而旧用户代码将正常工作。 :微笑:

这个真的很需要! 特别是对于具有返回实例的静态方法的 DAO。 它们中的大多数在基本 DAO 上定义,例如(保存、获取、更新等...)

我可以说它可能会引起一些混乱,将静态方法上的this类型解析为它所在的类而不是类的类型(即: typeof )。

在真正的 JS 中,对类型调用静态方法将导致函数内部的this是类而不是它的实例......所以它不一致。

但是,在人们的直觉中,我认为在静态方法上看到this返回类型时弹出的第一件事是实例类型......

@shlomiassaf这不会不一致。 当您将类指定为像 User 这样的函数的返回类型时,返回类型将是用户的实例。 完全相同,当您在静态方法上定义 this 的返回类型时,返回类型将是 this 的实例(类的实例)。 然后可以使用 typeof this 对返回类本身的方法进行建模。

@felixfbecker这完全是一个观点,这就是你选择看待它的方式。

让我们检查一下 JS 中发生了什么,这样我们就可以推断出逻辑:

class Example {
  myFunc(): this {
    return this; 
  }

  static myFuncStatic(): this {
    return this;   // this === Example
  }
}

new Example().myFunc() //  instanceof Exapmle === true
Example.myFuncStatic() // === Example

现在,实际运行时中的this是函数的有界上下文,这正是在类似接口的流式 API 中发生的情况,多态 this功能通过返回正确的类型来帮助,这与 JS 的工作方式保持一致,返回this的基类返回派生类创建的实例。 该功能实际上是一个修复程序。

总结:
返回this的方法被定义为原型(实例)的一部分,预计将返回实例。

继续该逻辑,返回this的方法被定义为类类型(原型)的一部分,预计将返回有界上下文,即类类型。

同样,没有偏见,没有意见,简单的事实。

直觉上,我会觉得从静态函数返回的this表示实例很舒服,因为它是在类型中定义的,但这就是我。 其他人可能会有不同的想法,我们不能告诉他们他们的错误。

问题是它需要既可以键入返回类实例的静态方法,也可以键入返回类本身的静态方法(typeof this)。 从 JS 的角度来看,您的逻辑是有道理的,但我们在这里讨论的是 return _types_,并且在 TypeScript 和任何其他语言中使用类作为返回类型(这里是 this)总是意味着类的实例。 要返回实际的类,我们有 typeof 运算符。

@felixfbecker这引发了另一个问题!

如果类型this是实例类型,则它不同于this关键字在静态方法主体中所指的内容。 return this产生一个typeof this的函数返回类型,这太奇怪了!

不,这不对。 当你定义一个方法时

getUser(): User {
  ...
}

您希望得到一个 User _instance_ ,而不是 User 类(这就是typeof的用途)。 这就是它在每种类型语言中的工作方式。 类型语义与运行时语义完全不同。

为什么不使用thisstatic关键字作为 func 中的构造函数来操作子类?

class Model {
  static find():this[] {
    return [new this("prop")]; // or new static(...)
  }
}

class Entity extends Model {
  constructor(public prop:string) {}
}

Entity.find().map(x => console.log(x.prop));

如果我们将其与 JS 中的示例进行比较,我们将看到它正确地工作:

class Model {
  static find() { 
    return [new this] 
  }
}

class Entity extends Model {
  constructor(prop) {
    this.prop = prop;
  }
}

Entity.find().map(x => console.log(x.prop))

您不能在静态方法上使用this类型,我认为这是问题的全部根源。

@felixfbecker

考虑一下:

class Greeter {
    static getHandle(): this {
        return this;
    }
}

这种类型注解很直观,但如果静态方法中的类型this是类实例则不正确。 关键字this typeof this类型!

我确实觉得this _should_ 指的是实例类型,而不是类类型,因为我们可以从实例类型( typeof X )中获取对类类型的引用,但反之则不行( instancetypeof X ?)

@xealot好吧,为什么不使用static关键字代替this ? 但是 JS 静态上下文中的this仍然指向一个构造函数。

@izatop是的,生成的 javascript 确实有效(正确或错误)。 但是,这与 Javascript 无关。 抱怨不是 Javascript 语言不允许这种模式,而是 Typescript 的类型系统不允许。

您无法使用此模式维护类型安全,因为 Typescript 不允许在静态成员上使用多态this类型。 这包括使用static关键字和constructor定义的类方法。

游乐场链接

@LPGhatguy ,但您确实阅读了我之前的评论,对吧?! 如果你有一个返回类型User的方法,你期望的是return new User(); ,而不是return User; 。 同样的方式返回值this应该意味着return new this();在静态方法中。 在非静态方法中这是不同的,但很明显,因为this始终是一个对象,您将无法实例化它。 但最后,我们基本上都同意这是最好的语法,因为typeof并且在 TS 中非常需要这个特性。

@xealot我了解 TypeScript 不允许多态this什么,但是我会问你为什么不将此功能添加到 TS 中?

我不知道以下内容是否适用于此问题的所有用例,但我发现在静态方法中使用this:类型以及智能使用类型推断系统可以带来乐趣打字。 它的可读性可能不太好,但它无需在子类中重新定义静态方法即可完成工作。

适用于[email protected]

考虑以下 ;

// IModelClass is just here to describe an instanciator
// since we can't use typeof T (unfortunately) with
// the generic type system.
interface IModelClass<T extends Model> {
  new (...a: any[]): T

  // unfortunately, we have to put here again all the typing information
  // of the static members (without static, since we are describing a class, not an instance)

  some_member: string
  create<T extends Model>(this: IModelClass<T>): T
}

class Model {

  // Here we use this with the IModel<T> to force the
  // type system to use T as our current caller.

  static some_member: string

  // When we call Dog.create() below, T is thus resolved
  // to Dog *and stays that way*
  // If typeof worked on generic types (it doesn't), we could have defined this method
  // instead as 
  // static create<T extends Model>(this: typeof T): T { ... }
  static create<T extends Model>(this: IModelClass<T>): T {
    return new this() // whatever you fancy here
  }
}

class Dog extends Model {
  bark() { }
}

class Cat extends Model {
  meow() { }
}

// Everything should be typed here, and we didn't have to redefine static methods
// in Dog nor Cat
let dog = Dog.create()
dog.bark()
let cat = Cat.create()
cat.meow()

@ceymard为什么将this作为参数给出?

这是由于https://github.com/Microsoft/TypeScript/issues/3694随 typescript 2.0.0 一起提供的,它允许为函数定义 this 参数(因此允许this在没有问题并防止错误使用这些功能)

事实证明,我们可以在方法和静态方法上使用它们,并且它允许真正有趣的东西,例如通过强制它比较我们提供的this来“帮助”类型推断器(模型) 到正在使用的那个(狗或猫)。 然后它“猜测” T的正确类型并使用它,从而正确键入我们的函数。

(注意this是 typescript 编译器理解的 _special_ 参数,它不会在函数中多加一个)

我认为语法类似于<this extends Whatever> 。 所以如果create()有其他参数,这仍然有效吗?

绝对地

我还想指出,这对于内置类型很重要。
例如,当您对Array进行子类化时,子类的from()方法将返回子类的实例,而不是Array

class Task {}
class TaskList extends Array<Task> {
  public execute() {}
}
// actually returns instance of TaskList at runtime
const tasks = TaskList.from([new Task()])
tasks.execute() // error, method execute does not exist on type Array

关于这个问题的任何更新? 此功能将极大地增强我的团队使用 TypeScript 的体验。

请注意,这是相当关键的,因为它是 ORM 中常用的模式,并且许多代码的行为与编译器和类型系统认为的非常不同。

我会提出一些针对反论点的反论点。

正如@shlomiassaf指出的那样,在静态成员中允许this会引起混淆,因为this在静态方法和实例方法中意味着不同的东西。 此外,类型注释中的 $# this this具有不同的语义。 我们可以通过引入新关键字来避免这种混淆,但是新关键字的成本很高。

在当前的 TypeScript 中有简单的解决方法。 @ceymard已经证明了这一点。 我更喜欢他的方法,因为它捕获了静态方法的运行时语义。 考虑这段代码。

class A {
  constructor() {}
  static create<T extends A>(this: {new (): T}) {} // constructor signature is exactly the same as A's
}

class B extends A {
  constructor(a: number) {
    super()
  }
}

B.create() // correctly trigger compile error here

如果支持多态,编译器应该在class B extends A中报告错误。 但这是一个突破性的变化。

@HerringtonDarkholme我没有看到混淆, this的返回类型正是我对return new this()的期望。 可能我们可以为此使用self ,但引入另一个关键字(除了用于产生输出的关键字之外)似乎更让我感到困惑。 我们已经将它用作返回类型,唯一(但很大)的问题是静态成员不支持它。

编译器不应该要求程序员做变通方法,TypeScript 应该是“增强的 JavaScript”,在这种情况下这不是真的,你必须为你的代码做一个复杂的变通方法才能不产生数百万个错误。 很高兴我们可以在当前版本中以某种方式实现它,但这并不意味着它很好并且不需要修复。

那么为什么this的返回类型与方法体中this的类型不同呢?
为什么这段代码会失败? 你怎么能向新来的人解释呢?

class A {
  static create(): this {
     return this
  }
}

为什么 JavaScript 的超集不能接受这个?

class A {
  static create() {
    return new this()
  }
}

abstract class B extends A {}

没错,我们可能需要引入另一个关键字。 可能是self ,也可能是instance

@HerringtonDarkholme这是我向新人解释的方式。
当你这样做

class A {
  static whatever(): B {
    return new B();
  }
}

B的返回类型意味着您需要返回一个B的实例。 如果要返回类本身,则需要使用

class B {
 static whatever(): typeof B {
   return B;
 }
}

现在,就像在 JavaScript 中一样,静态成员中的this指的是类。 因此,如果您在静态方法中使用this的返回类型,则必须返回该类的实例:

class A {
  static whatever(): this {
    return new this();
  }
}

如果要返回类本身,则需要使用typeof this

class A {
  static whatever(): typeof this {
    return this;
  }
}

从那时起,这正是类型在 TS 中的工作方式。 如果您使用类键入,则 TS 需要一个实例。 如果要键入类类型本身,则需要使用typeof

然后我会问为什么this在方法体中没有相同的含义。 如果实例方法的类型注解中的 $# this 1$#$ 与对应的方法体中的this含义相同,为什么静态方法不一样呢?

我们面临两难境地,有三种选择:

  1. make this在不同的上下文中表示不同的东西(混淆)
  2. 引入一个新的关键字,如selfstatic
  3. 让程序员写更多怪异的类型注解(影响部分用户)

我更喜欢第三种方法,因为它反映了静态方法定义的运行时语义
它还指出了使用站点上的错误,而不是定义站点,也就是说,就像在这个评论中一样,错误是在B.create中报告的,而不是在class B extends A中报告的。 在这种情况下,使用站点错误更准确。 (假设您声明了一个引用this的静态方法,然后声明了一个抽象子类)。

而且,最重要的是,它不需要新功能的语言提案。

事实上,偏好因人而异。 但至少我希望看到这样一个更详细的提案。 有很多问题需要解决,比如抽象类可分配性、子类不兼容的构造函数签名和错误报告策略。

@HerringtonDarkholme因为在实例方法的情况下,运行时this是一个对象实例而不是一个类,所以没有混淆的空间。 很明显, this类型实际上是运行时this ,没有其他选择。 而在静态情况下,它的行为与任何面向对象编程语言中的任何其他类注释一样:使用类对其进行注释,并且您期望返回一个实例。 另外,在 TS 案例中,因为在 JavaScript 中类也是对象,所以使用typeof类键入它,然后您将返回文字类。

我的意思是,当他们实现this返回类型以考虑静态情况并要求用户始终为实例方法编写typeof this时,它可能会更加一致。 但是那个决定是在那时做出的,所以我们现在必须使用它,正如我所说,它并没有真正留下任何混淆的余地,因为实例方法中的this不会产生任何其他类型(不像类,谁有一个静态类型和一个实例类型),所以在这种情况下它是不同的,不会混淆任何人。

好的,我们假设没有人会被this弄糊涂。 抽象类可分配性如何?

方法的静态性有些随意。 任何函数都可以有属性和方法。 如果它也可以用new调用,我们选择调用这些属性和方法static 。 现在这个约定已经牢牢地融入了 ECMAScript。

@HerringtonDarkholme毫无疑问,使用this会引起混淆是正确的。 但是,由于使用this并没有什么不妥,我会说它完全没问题,尤其是考虑到您对各种替代方案的精明成本效益分析时。 我认为this是最不坏的选择。

我只是试图从我的原始帖子中赶上这个线程,我觉得这可能有点偏离轨道。

为了澄清,希望this类型注释在构造函数中使用时是多态的,特别是作为泛型/模板。 关于@shlomiassaf@HerringtonDarkholme ,我相信使用static方法的示例可能会令人困惑,这不是这个问题的意图。

尽管通常不这么认为,但类的构造函数是静态的。 该示例(我将用更清晰的评论重新发布)不是在静态方法上声明this ,而是通过静态方法的类型注释中的泛型声明this以供将来使用.

区别在于我不希望this立即在静态方法上计算,而是在未来以动态方法计算。

// START LIBRARY CODE
// Constrains the constructor to one that creates things that extend from BaseModel
interface ModelConstructor<T extends BaseModel> {
    new(fac: ModelAPI<T>): T;
}

class ModelAPI<T extends BaseModel> {
    // skipping the use of a ModelConstructor in favor of typeof does not work
    // constructor(private modelType: typeof T) {}
    constructor(private modelType: ModelConstructor<T>) {}

    create() {
        return new this.modelType(this);
    }
}

class BaseModel {
    // This is where "polymorphic `this`" in static members matters. We are 
    // trying to say that the ModelAPI should create instances of whatever 
    // the *current* class is, not the BaseModel class. Much like it would 
    // at runtime.
    constructor(private fac: ModelAPI<this>) {}

    reload() {
        // `reload()` returns a new instance of type Any, incorrect
        return this.fac.create();
    }
}
// END LIBRARY CODE

// START APPLICATION CODE
// Create a custom model class with custom behavior
class Model extends BaseModel {}

// Create an instance of the model API that produces my custom type
let api = new ModelAPI<Model>(Model);  // ModalAPI should be able to infer "<Model>" from the constructor?
let modelInst = api.create();  // Returns type of Model, correct
let reset = modelInst.reload();  // Returns type of Any, incorrect
// END APPLICATION CODE

对于任何认为这令人困惑的人,好吧,我同意这不是超级直截了当。 但是,在BaseModel的构造函数中使用this并不是真正的静态使用,而是稍后计算的延迟使用。 但是,静态方法(包括构造函数)不是这样操作的。

我认为在运行时这一切都按预期工作。 然而,类型系统无法对这种特定模式进行建模。 typescript 无法为 javascript 模式建模是此问题开放的基础。

抱歉,如果这是一个令人困惑的评论,写得很仓促。

@xealot我明白你的意思,但其他特别建议多态this用于静态方法的问题已作为此问题的副本关闭。 我假设 TS 中的修复将启用这两个用例。

您的确切用例是什么? 也许“这个”解决方案就足够了。

我在自定义 ORM 库中成功使用它
乐。 10 月 20 日 2016 à 19:15,Tom Marius [email protected]
评价:

请注意,这是相当重要的,因为它是常用的
ORM 中的模式和许多代码的行为与
编译器和类型系统认为它应该。


你收到这个是因为你被提到了。
直接回复此邮件,在 GitHub 上查看
https://github.com/Microsoft/TypeScript/issues/5863#issuecomment -255169194,
或使线程静音
https://github.com/notifications/unsubscribe-auth/AAtAoRHc45B386xdNhuCLB8iW6i82e7Uks5q16GzgaJpZM4GsmOH
.

感谢@ceymard和@HerringtonDarkholme。

static create<T extends A>(this: {new (): T}) { return new this(); }帮了我大忙👍

乍一看,它比static create(): this { return new this(); }更晦涩难懂,但至少它是正确的! 它准确地捕获了静态方法中this的类型。

我希望以某种方式可以优先考虑这一点,即使它不是最受支持或评论的等等。不得不使用类型参数来获得正确的类型是非常烦人的,例如:

let u = User.create<User>();

当实际上可以这样做时:

let u = User.create();

不可避免的是,静态有时指的是实例类型。

关于静态成员中的this类型应该是类的静态端还是实例端的类型,我有另一个想法。 this可以是静态的一面,它将匹配实例成员中所做的事情以及运行时行为是什么,然后您可以通过this.prototype获取实例类型:

abstract class Model {
  static findAll(): Promise<this.prototype[]>
}

class User extends Model {}

const users: User[] = await User.findAll();

这与它在 JavaScript 中的工作方式是一致的。 this是类对象本身,实例位于this.prototype上。 它本质上与映射类型的机制相同,相当于this['prototype']

以同样的方式,您可以通过this.constructor想象静态方面在实例成员中可用(我想不出这个 atm 的用例)。

我还想提一下,在 Sequelize 中输入 ORM 模型时,所提到的解决方法/hacks 都不起作用。

  • this类型注释不能在构造函数中使用,因此它不适用于此签名:
    ts abstract class Model { constructor(values: Partial<this.prototype>) }
  • 将类型作为泛型类参数传递不适用于静态方法,因为静态成员不能引用泛型参数
  • 将类型作为泛型方法参数传递适用于方法,但不适用于静态属性,例如:
    ts abstract class Model { static attributes: { [K in keyof this.prototype]: { type: DataType, field: string, unique: boolean, primaryKey: boolean, autoIncrement: boolean } }; }

对此请求 +1。 我的 ORM 会很干净。

希望我们能尽快看到干净的解决方案

同时,感谢所有提供解决方案的人!

既然没有更多的讨论,我们可以进一步讨论吗?

你想把它带到哪里? 据我所知,这仍然是 Typescript 无法干净地为 Javascript 建模的一种方式,并且除了根本不这样做之外,还没有看到任何解决方法。

这是 ORM 开发人员必备的功能,因为我们可以看到已经有三个流行的 ORM 所有者要求使用此功能。

@pleerock @xealot解决方案已在上面提出:

export type StaticThis<T> = { new (): T };

export class Base {
    static create<T extends Base>(this: StaticThis<T>) {
        const that = new this();
        return that;
    }
    baseMethod() { }
}

export class Derived extends Base {
    derivedMethod() { }
}

// works
Base.create().baseMethod();
Derived.create().baseMethod();
// works too
Derived.create().derivedMethod();
// does not work (normal)
Base.create().derivedMethod();

我正在广泛使用它。 基类中静态方法的声明有点繁重,但这是为了避免在静态方法中this的类型失真而付出的代价。

@pleerock

我有一个内部 ORM,它广泛使用this:模式而没有问题。

我认为当功能实际上已经存在时,没有必要对语言过度收费,尽管承认有点令人费解。 更清晰语法的用例非常有限,恕我直言,可能会引入不一致。

也许可以有一个 Stackoverflow 线程与这个问题和解决方案供参考?

@bjouhier @ceymard我解释了为什么此线程中的所有解决方法都不适用于 Sequelize: https ://github.com/Microsoft/TypeScript/issues/5863#issuecomment -269463313

这不仅仅是关于 ORM,还有标准库: https ://github.com/Microsoft/TypeScript/issues/5863#issuecomment -244550725

@felixfbecker刚刚发布了一些关于构造函数用例的内容并将其删除(我意识到 TS 在发布后并没有强制每个子类中的构造函数)。

@felixbecker关于构造函数的情况,我通过坚持无参数构造函数并提供专门的静态方法(创建,克隆,...)来解决它。 更明确,更容易输入。

@bjouhier这意味着您必须基本上重新声明每个模型子类中的每个方法。 看看 Sequelize 中有多少: http ://docs.sequelizejs.com/class/lib/model.js~Model.html

我的论点是从完全不同的角度出发的。 虽然存在部分且有些不直观的解决方法,我们可以就这些是否足够存在争论和分歧,但我们可以同意这是 Typescript 无法轻松建模 Javascript 的领域。

而我的理解是 Typescript 应该是 Javascript 的扩展。

@felixfbecker

这意味着您必须基本上重新声明每个模型子类中的每个方法。

为什么? 这根本不是我的经验。 你能用代码示例说明问题吗?

@bjouhier我在此线程的评论中用代码示例深入说明了这个问题,首先https://github.com/Microsoft/TypeScript/issues/5863#issuecomment -222348054

但是看看我上面的create例子create方法是一个静态方法。 它没有重新声明,但在子类中正确键入。 那么为什么Model的方法需要重新定义呢?

@felixfbecker你的_作为一个开始_的例子:

export type StaticThis<T> = { new (): T };

abstract class Model {
    public static tableName: string;
    public static findById<T extends Model>(this: StaticThis<T>, id: number): T {
        const instance = new this();
        // details omitted
        return instance;        
    }
}

class User extends Model {
    public static tableName = 'users';
    public username: string;
}

const user = User.findById(1); 
console.log(user.username);

@bjouhier好的,看起来this: { new (): T }注释实际上使类型推断起作用。 这让我想知道为什么这不是编译器使用的默认类型。
解决方法当然不适用于构造函数,因为它们不能有this参数。

是的,它不适用于构造函数,但是您可以通过添加一个静态create方法来通过一个小的 API 更改来解决这个问题。

我理解,但这是一个 JavaScript 库,所以我们正在讨论在声明中键入现有 API。

如果this { new (): T } ,那么this将不是静态方法中this的正确类型。 例如new this()是不允许的。 为了一致性, this必须是构造函数的类型,而不是类实例的类型。

我遇到了键入现有库的问题。 如果您无法控制该库,您仍然可以使用正确类型的create函数创建中间基类 ( BaseModel extends Model ),并从BaseModel派生所有模型。

如果要访问基类的静态属性,可以使用

public static findById<T extends Model>(this: (new () => T) & typeof Model, id: number): T {...}

但是您可能对构造函数有一个有效的观点。 我看不出编译器必须拒绝以下内容的硬性原因:

constructor(values: Partial<this>) {}

我同意@xealot的观点,这里有痛苦。 如果我们能写就太好了

static findById(id: number): instanceof this { ... }

代替

static findById<T extends Model>(this: (new () => T), id: number): T { ... }

但是我们需要在打字系统中添加一个新的运算符( instanceof ?)。 以下内容无效:

static findById(id: number): this { ... }

TS 2.4 为我打破了这些解决方法:
https://travis-ci.org/types/sequelize/builds/247636686

@sandersn @felixfbecker我认为这是一个有效的错误。 但是,我无法以最小的方式复制它。 回调参数是逆变的。

// Hooks
User.afterFind((users: User[], options: FindOptions) => {
  console.log('found');
});

这有可能在某个时候得到解决吗?

我今天也遇到了这个。 基本上我想构建一个单例基类,如下所示:

abstract class Singleton<T> {
  private static _instance?: T

  public static function getInstance (): T {
    return this._instance || (this._instance = new T())
  }
}

用法类似于:

class Foo extends Singleton<Foo> {
  bar () {
    console.log('baz!')
  }
}

Foo.getInstance().bar() // baz!

我尝试了大约 10 种变体,上面提到的StaticThis变体以及许多其他变体。 最后只有一个版本甚至可以编译,但是getInstance()的结果被导出为Object

我觉得这比使用这些构造要困难得多。

以下作品:

class Singleton {
    private static _instance?: Singleton;

    static getInstance<T extends Singleton>(this: { new(): T }) {
      const constr = this as any as typeof Singleton; // hack
      return (constr._instance || (constr._instance = new this())) as T;
    }
  }

  class Foo extends Singleton {
    foo () { console.log('foo!'); }
  }

  class Bar extends Singleton {
    bar () { console.log('bar!');}
  }

  Foo.getInstance().foo();
  Bar.getInstance().bar();

this as any as typeof Singleton部分很难看,但它表明我们在欺骗类型系统,因为_instance必须存储在派生类的构造函数中。

通常,基类将隐藏在您的框架中,因此如果它的实现有点难看,它不会造成太大的伤害。 重要的是派生类中的干净代码。

我希望在 2.8 nightlies 中能够做这样的事情:

static findById(id: number): InstanceType<this> { ... }

可悲的是没有这样的运气:(

@bjouhier代码示例就像一个魅力。 但打破 IntelliSense 自动完成功能。

React 16.3.0 发布了,似乎不可能正确键入新的getDerivedStateFromProps方法,因为静态方法不能引用类类型参数:

(简化示例)

class Component<P, S> {

    static getDerivedStateFromProps?<K extends keyof S>(nextProps: P, prevState: S): Pick<S, K> | null

    props: P
    state: S
}

有什么解决方法吗? (我不认为“将 P 和 S 键入为 PP 和 SS,并希望开发人员能够正确地使这两个类型系列完全匹配”作为一个:p)

公关: https ://github.com/DefinitelyTyped/DefinitelyTyped/pull/24577

@cncolder我也见证了智能感知的崩溃。 我相信它是从 typescript 2.7 开始的。

我在这里看不到的一件事,在 Singleton 示例上进行了扩展-使 Singleton / Multiton 类具有受保护的构造函数将是一种常见模式-如果类型为{ new(): T }则无法扩展,因为 this强制执行公共构造函数 - this术语或此处提供的其他解决方案( instanceof thistypeof this等)应该可以解决该问题。 如果单例没有请求创建它等,那么在构造函数中抛出错误绝对是可能的,但这违背了输入错误的目的 - 有一个运行时解决方案......

class Singleton {

  private static _instance?: Singleton;

  public static getInstance<T extends Singleton> ( this: { new(): T } ): T {
    const ctor: typeof Singleton = this as any; // hack
    return (ctor._instance || (ctor._instance = new this())) as T;
  }

  protected constructor ( ) { return; } 
}

class A extends Singleton {
  protected constructor ( ) {
    super();
  }
}

A.getInstance() // fails because constructor is not public

我不知道为什么你需要一个带有getInstance()的 TS 中的单例,你可以定义一个对象文字。 或者,如果您绝对想使用类(对于私有成员),只导出类实例而不导出类,或者使用类表达式( export new class ... )。

这对 Multiton 更有用 - 我相信我已经找到了一种有效的模式,......虽然没有这种类型它仍然是非常 hacky 的感觉......相反,这是必要的

this -> { new(): T } & typeof Multiton | Function

class Multiton {
  private static _instances?: { [key: string]: any };

  public static getInstance<T extends Multiton> (
    this: { new(): T } & typeof Multiton | Function, key: string
  ): T {
    const instances: { [key: string]: T } =
      (this as typeof Multiton)._instances ||
     ((this as typeof Multiton)._instances = { });

    return (instances[key] ||
      (instances[key] = new (this as typeof Multiton)() as T)
    );
  }

  protected constructor ( ) { return; }
}

class A extends Multiton {
  public getA ( ): void { return; }
}
A.getInstance("some-key").getA();
assert(A.getInstance("some-key") === A.getInstance("some-key"))
new A(); // type error, protected constructor

我还有一些代码来存储密钥,以便在创建时可以在子类中访问它,但为了简单起见,我已经将其删除了......

你好。 我遇到了一个需要构造函数的多态“this”的问题。 类似于此评论。

我想创建一个类。 类本身有很多属性,因此如果该类的所有属性都通过构造函数的参数进行初始化,构造函数将不会看起来很好。

我更喜欢接受对象来初始化属性的构造函数。 例如:

class Vehicle {
  // many properties here
  // ...

  constructor(value: Partial<Vehicle>) {
      Object.assign(this, value)
  }
}

let vehicle = new Vehicle({
  // <-- IntelliSense works here
})

然后,我想扩展课程。 例如:

class Car extends Vehicle {
  // more properties here
  // ...
}

let car = new Car({
  // <-- I can't infer Car's properties here
})

如果在构造函数上也可以使用this类型,那就太好了。

class Vehicle {
  // ...
  constructor(value: Partial<this>) {
      // ...

因此,IntelliSense 无需额外的样板即可推断子类的属性:例如,重新定义构造函数。 它看起来比使用静态方法初始化类更自然。

let car = new Car({
  // <-- Car's properties will be able to be inferred if Partial<this> is allowed
})

非常感谢您的考虑。

我很高兴我找到了这个线程。 老实说,我认为这是一个非常严重的问题,看到它延续到 3.0 时非常沮丧。

查看所有解决方案并考虑我在过去几年中尝试过的各种解决方法,我得出的结论是,如果解决方案到位,我们尝试的任何快速修复都只会失败。 在那之前,我们所做的每一次强制都需要太多的手动工作才能在整个项目中维护它。 而且一旦发布,你就不能要求消费者了解你自己的心态,以及为什么你选择修复 TypeScript 的东西,如果他们只是在 vanilla 中使用你的包,但像我们大多数人一样,使用 VSCode。

同时,当实际语言不是问题时,编写代码让类型系统满意,这与一开始就使用类型化特性的原因是完全相反的。

我对每个人的建议是接受波浪线有缺陷并尽可能使用//@ts-ignore because my code works as expected

如果你的 linter 对合理的人类智能免疫,那么是时候找到一个知道它的位置的了。

更新
我想添加我自己的尝试来可能安全地增强静态推理。 这种方法严格来说是环境的,因为它旨在不碍事。 它非常明确,迫使您检查您的扩充,而不仅仅是假设继承会为您完成工作。

export class Sequence<T> {
  static from(...values) {
    // … returns Sequence<T>
  };
}

export class Peekable<T> extends Sequence<T> {
  // no augmentations needed in actual class body
}

/// AMBIENT /// Usually keep those at the bottom of my files

export declare namespace Peekable {
  export function from<T>(... values: T[]): Peekable<T>;
}

显然,我不能保证这种模式成立,也不能保证它会满足这个线程中的每个场景,但现在,它可以按预期工作。

这些选择源于一个令人担忧的认识,即到目前为止,TypeScript 自己的 lib 文件只包含两个类声明!

安全阵列

/**
 * Represents an Automation SAFEARRAY
 */
declare class SafeArray<T = any> {
    private constructor();
    private SafeArray_typekey: SafeArray<T>;
}

变量日期

/**
 * Automation date (VT_DATE)
 */
declare class VarDate {
    private constructor();
    private VarDate_typekey: VarDate;
}

今天讨论了一段时间。 关键点:

  • this是指实例端还是静态端?

    • 绝对是静态的一面。 实例端可以通过InstanceTypeOf引用,但反之则不然。 这也保持了签名中的 $# this 2$#$ 与正文中的this具有相同类型的对称性。

  • 健全性问题

    • 零保证派生类构造函数参数列表与其基类构造函数参数列表有任何关系

    • 在这方面,类经常破坏静态方面的可替代性

    • 强制执行非构造方面的可替代性,这通常人们感兴趣的东西

    • 我们已经允许在static方法中不合理地调用new this() $

    • 可能没有人从外部关心静态this的构造签名方面吗?

现有的解决方法:

class Foo {
    static boo<T extends typeof Foo>(this: T): InstanceType<T> {
        return (new this()) as InstanceType<T>;
    }
}

class Bar extends Foo {
}

// b: Bar
let b = Bar.boo();

值得注意的是,这仅在Bar的构造签名可以正确替换Foo的情况下才有效

@RyanCavanaugh我一直在尝试解释this的值来自您的示例static boo<T extends typeof Foo>(this: T): InstanceType<T> ,但我就是不明白。 可以肯定的是,我重新阅读了有关泛型的所有内容,但仍然 - 不。 例如,如果我用 $#$ clazz $#$ 替换this ,它将不再起作用,编译器会抱怨“预期 1 个参数,但得到 0”。 所以那里正在发生一些魔法。 你能解释一下吗? 或者在文档中指出我正确的方向?

编辑:
另外,为什么是extends typeof Foo而不仅仅是extends Foo

@creynders this是一个特殊的假参数,可以为函数的上下文输入。 文档

当涉及泛型时,使用InstanceType<T>不起作用。

我的用例如下所示:

const _data = Symbol('data');

class ModelBase<T> {
    [_data]: Readonly<T>;

    protected constructor(data: T) {
        this[_data] = Object.freeze(data);
    }


    static create<T, V extends typeof ModelBase>(this: V, data : T): InstanceType<V> {
        return new this(data);
    }
}

interface IUserData {
    id: number;
}

class User extends ModelBase<IUserData> {}

User.create({ id: 5 });

TypeScript 游乐场链接

@mastermatt哦,哇,在阅读文档时完全没有意识到这一点......谢谢

查看@RyanCavanaugh的示例,我能够让我们的静态方法正常工作,但我也有引用静态方法的实例方法,但我不知道如何为此获得正确的输入。

例子:

class Foo {
    static boo<T extends typeof Foo>(this: T): InstanceType<T> {
        return (new this()) as InstanceType<T>;
    }

    boo<T extends typeof Foo>(this: InstanceType<T>): InstanceType<T> {
      return (this.constructor as T).boo();
    }
}

class Bar extends Foo {
}

// b: Bar
let b = Bar.boo();
// c: Foo
let c = b.boo();

如果我能找到最后一个问题的解决方法,我会等到官方出现。

同样值得注意的是,如果子类尝试覆盖任何静态方法然后调用super ,那么它也会变得很沮丧......除了这两个问题之外,解决方法似乎工作正常。 虽然,这两个问题对我们的代码来说是相当阻塞的。

IIRC 这是因为如果不稍微弯曲类型系统,这在打字稿中是无法表达的。 如 ; this.constructor 完全不能保证是你认为的那样或类似的东西。

在那种精确的情况下,我会让boo()实例方法返回this并在里面作弊,迫使编译器接受我知道我在做什么。

我的一般推理是,我希望 API 对于其余代码尽可能简单,即使有时这意味着作弊。

如果有人有更强大的东西,我会喜欢的

class Foo {
  static boo<T extends typeof Foo>(this: T): InstanceType<T> {
      return (new this()) as InstanceType<T>;
  }

  boo(): this {
    // @ts-ignore : wow this is ugly 
    return (this.constructor).boo();
  }
}

class Bar extends Foo {
}

// b: Bar
let b = Bar.boo();
// c: Bar !
let c = b.boo();

您也可以走接口路线并做类似的事情;

interface FooMaker<T> {
  new(...a: any[]): T
  boo(): T
}


class Foo {
  static boo<T extends typeof Foo>(this: T): InstanceType<T> {
      return (new this()) as InstanceType<T>;
  }

  boo(): this {
    return (this.constructor as FooMaker<this>).boo();
  }
}

class Bar extends Foo {
}

// b: Bar
let b = Bar.boo();
// c: Bar !
let c = b.boo();

这也是作弊,但也许更受控制? 我真的不能说。

因此,静态“boo”方法中的第一个this参数被特殊处理,而不是常规参数? 任何指向文档的链接,描述这个?

class Foo {
    static boo<T extends typeof Foo>(this: T): InstanceType<T> {
        return (new this()) as InstanceType<T>;
    }
}

class Bar extends Foo {
}

// b: Bar
let b = Bar.boo();

我有同样的(至少类似的)问题。
我将 Vanilla Javascript 与 JSDoc(不是 TypeScript)一起使用,因此我无法使用/实现围绕该线程中提出的泛型的变通办法,但根似乎是相同的。
我已经打开了这个问题:#28880

这个问题已经有3年了。
有没有人找到适合我的解决方法?

三年

@RyanCavanaugh的解决方法非常适合静态方法,但是静态访问器呢?

class Factory<T> {
  get(): T { ... }
}

class Base {
  static factory<T extends Base>(this: Constructor<T>): Factory<T> {
    //
  }

  // what about a getter?
  static get factory<** no generics allowed for accessors **> ...
}

三年

好吧,你在减去日期方面做得还不错。 但是你能提供一个解决方案吗? ;)

@RyanCavanaugh当我们在生成器中使用this值时,这是一个问题,如下所示:

class C {
  constructor(f: (this: this) => void) {
  }
}
new C(function* () {
  this;
  yield;
});

当我们制作生成器时,我们不能使用箭头函数来使用this值。 所以现在我们必须在子类中显式声明this类型。 一个实际案例如下:

      class Component extends Coroutine<void> implements El {
        constructor() {
          super(function* (this: Component) {
            while (true) {
              yield;
            }
          }, { size: Infinity });
        }
        private readonly dom = Shadow.section({
          style: HTML.style(`ul { width: 100px; }`),
          content: HTML.ul([
            HTML.li(`item`)
          ] as const),
        });
        public readonly element = this.dom.element;
        public get children() {
          return this.dom.children.content.children;
        }
        public set children(children) {
          this.dom.children.content.children = children;
        }
      }

https://github.com/falsandtru/typed-dom/blob/v0.0.134/test/integration/package.ts#L469

如果我们可以在基类中做到这一点,我们就不必在子类中声明this类型。 但是,在超类中同步调用生成器时, this的值并没有被初始化。 我在代码中避免了这个问题,但这是一个肮脏的黑客。 所以这种模式最初可能与基于类的编程语言不匹配。

不是最干净的,但对于从静态方法访问子类可能很有用。

class Base {
    static foo<T extends typeof Base>() {
        let ctr = Object.create(this.prototype as InstanceType<T>).constructor;
        // ...
    }
}

class C extends Base {
}

C.foo();

虽然我不确定我目前面临的问题是否是由于这个,但在简要浏览一下这个问题后,似乎可能是这样。 如果不是这种情况,请更正。

所以我有以下两个通过继承相关的类。

export class Target {
  public static create<T extends Target = Target>(that: Partial<T>): T {
    const obj: T = Object.create(this.prototype);
    this.mapObject(obj, that);
    return obj;
  }
  public static mapObject<T extends Target = Target>(obj: T, that: Partial<T>) {
    // works with "strictNullChecks": false
    obj.prop1 = that.prop1;
    obj.prop2 = that.prop2;
  }

  public prop1!: string;
  constructor(public prop2: string) {}
}

export class SubTarget extends Target {
  public subProp!: string;
}

接下来,我在SubTarget类中添加一个mapObject方法,如下所示。

  public static mapObject(obj: SubTarget, that: Partial<SubTarget>) {
    super.mapObject(obj, that);
    obj.subProp = that.subProp;
  }

虽然我希望这可以工作,但我收到以下错误。

Class static side 'typeof SubTarget' incorrectly extends base class static side 'typeof Target'.
  Types of property 'mapObject' are incompatible.
    Type '(obj: SubTarget, that: Partial<SubTarget>) => void' is not assignable to type '<T extends Target>(obj: T, that: Partial<T>) => void'.
      Types of parameters 'obj' and 'obj' are incompatible.
        Type 'T' is not assignable to type 'SubTarget'.
          Property 'subProp' is missing in type 'Target' but required in type 'SubTarget'.

这个错误是由于这个问题而产生的吗? 否则,解释将非常棒。

链接到游乐场

在寻找注释静态方法的解决方案时来到这里,这些方法返回被调用的实际类的新实例。

我认为@SMotaal建议的解决方法(结合new this() )是最干净的,对我来说最有意义。 它允许表达所需的意图,不会强迫我在每次调用泛型方法时指定类型,并且不会在最终代码中增加任何开销。

但说真的,这还不是核心 Typescript 的一部分吗? 这是相当常见的情况。

@danielvy — 我认为 OOP 和原型之间的脱节是更大的心理差距,无法让每个人都开心。 我认为关于类的核心假设早在规范中的类特性演变之前就已经出现了,打破这一点是许多工作 TypeScript 特性的陷阱,所以每个人都在“优先考虑”,这没关系,但不是“类型”在我看来。 没有比竞争更好的解决方案只有在存在竞争时才会成为问题,这就是为什么一个篮子里的所有鸡蛋总是对每个人都不利的原因——我在这里是务实和真诚的。

这不起作用:

export class Base {
  static getEntitySchema<T extends typeof Base>(
    this: T,
  ): InstanceType<T> {
  }
}

export class Extension extends Base {
  static member: string = '';
  static getEntitySchema<T extends typeof Extension>(
    this: T,
  ): InstanceType<T> {
  }
}

Type 'typeof Base' is missing the following properties from type 'typeof Extension ': member.

但是,既然 Extension 扩展了 Base,难道不应该允许这样做吗?

请注意这有效:

export class Extension extends Base {
  static member: string = '';
  static getEntitySchema<T extends typeof Base>(
    this: T,
  ): InstanceType<T> {
  }
}

但是你不能在里面使用 this.member 。

所以我从@ntucker帖子中猜测,除非扩展类与基类完全匹配,否则解决方法只能工作一层深?

你好! 受保护的构造函数呢?

class A {
  static create<T extends A>(
    this: {new(): T}
  ) {
    return new this();
  }

  protected constructor() {}
}

class B extends A {} 

B.create(); // Error ts(2684)

如果构造函数是公共的 - 没关系,但如果不是,则会导致错误:

The 'this' context of type 'typeof B' is not assignable to method's 'this' of type '(new () => B) & typeof A'.
  Type 'typeof B' is not assignable to type 'new () => B'.
    Cannot assign a 'protected' constructor type to a 'public' constructor type.

游乐场 - http://tiny.cc/r74c9y

这有希望吗? 再次偶然发现了这种需求:F

@ntucker Duuuuudee,你做到了!!! 对我来说,关键的实现是使静态创建方法通用,声明this参数,然后在最后进行InstanceType<U>强制转换。

操场

从上面 ^

class Base<T> {
  public static create<U extends typeof Base>(
    this: U
  ) {
    return new this() as InstanceType<U>
  }
}
class Derived extends Base<Derived> {}
const d: Derived = Derived.create() // works 😄 

这完全满足了我的用例,并且似乎从 TS 3.5.1 开始,这也满足了该线程中的大多数 ppl(请参阅操场探索静态道具theAnswer以及第三级扩展)

热门话题:我认为这现在有效并且可以关闭?

@jonjaques顺便说一句,您从不使用 T。此外,这并不能解决我概述的覆盖方法的问题。

我觉得上面提到的解决方案已经在大约 4 年前提出......但这并不是解决问题的方法。

我打开了一个stackoverflow ,看看是否有人可能知道其他解决方法,并且有人对我遇到的问题有一个很好的总结:

“用作方法参数的泛型,包括这个参数,似乎是逆变的,但实际上你总是希望这个参数被视为协变的(甚至可能是双变的)。”

所以看起来这确实是 TypeScript 本身被破坏的东西,应该修复(“this”应该是协变的或双变的)。

编辑:此评论有更好的方法。

我没有太多要补充的,但这对我来说很有效,来自https://github.com/microsoft/TypeScript/issues/5863#issuecomment -437217433

class Foo {
    static create<T extends typeof Foo>(this: T): InstanceType<T> {
        return (new this()) as InstanceType<T>;
    }
}

class Bar extends Foo { }

// typeof b is Bar.
const b = Bar.create()

希望这对不想浏览这个冗长问题的人有用。

另一个用例是非类上的自定义类型保护成员函数:

type Baz = {
    type: "baz"
}
type Bar = {
     type: "bar"
}
type Foo = (Baz|Bar)&{
    isBar: () => this is Bar 
}

解决方法几乎可以让我到达我需要的地方,但我还有一个扳手可以插入齿轮。 举一个人为的例子:

interface IAutomobileOptions {
  make: string
}

interface ITruckOptions extends IAutomobileOptions {
  bedLength: number
}

export class Automobile<O extends IAutomobileOptions> {
  constructor(public options: O) {}

  static create<T extends typeof Automobile, O extends IAutomobileOptions>(
    this: T,
    options: O
  ): InstanceType<T> {
    return new this(options) as InstanceType<T>
  }
}

export class Truck<O extends ITruckOptions> extends Automobile<O> {
  constructor(truckOptions: O) {
    super(truckOptions)
  }
}

const car = Automobile.create({ make: 'Audi' })
const truck = Truck.create({ make: 'Ford', bedLength: 7 }) // TS Error on Truck

这是错误:

The 'this' context of type 'typeof Truck' is not assignable to method's 'this' of type 'typeof Automobile'.
  Types of parameters 'truckOptions' and 'options' are incompatible.
    Type 'O' is not assignable to type 'ITruckOptions'.
      Property 'bedLength' is missing in type 'IAutomobileOptions' but required in type 'ITruckOptions'.ts(2684)

这对我来说很有意义,因为Automobile类中的create方法没有通过O引用ITruckOptions #$ ,但我想知道是否有该问题的解决方法?

通常,我的构造函数的扩展类选项将扩展基类的选项接口,所以我知道它们将始终包含基类的参数,但没有可靠的方法来确保它们包含扩展的参数班级。

我还不得不求助于扩展类中的重写方法,以通知它们继承类的预期输入和返回类型,这感觉有点臭。

@Jack-Barry 这行得通:

interface IAutomobileOptions {
  make: string
}

interface ITruckOptions extends IAutomobileOptions {
  bedLength: number
}

export class Automobile<O extends IAutomobileOptions> {
  constructor(public options: O) { }

  static create<T extends Automobile<O>, O extends IAutomobileOptions>(
    this: { new(options: O): T; },
    options: O
  ): T {
    return new this(options)
  }
}

export class Truck<O extends ITruckOptions> extends Automobile<O> {
  constructor(truckOptions: O) {
    super(truckOptions)
  }
}

const car = Automobile.create({ make: 'Audi' });
const truck = Truck.create({ make: 'Ford', bedLength: 7 });

但是,它仍然不理想,因为构造函数是公共的,所以可以只做new Automobile({ make: "Audi" })

@elderapo我认为这让我得到了我需要的东西——这个例子当然是人为的,但我看到我对_Generics_ 缺乏了解让我有点吃不消。 感谢您清理它!

希望这对其他人有所帮助,如果有任何改进的余地,请告诉我。 这并不完美,因为读取功能可能会与打字不同步,但它是我能得到的最接近我们需要的模式。

// Interface to ensure attributes that exist on all descendants
interface IBaseClassAttributes {
  foo: string
}

// Type to provide inferred instance of class
type ThisClass<
  Attributes extends IBaseClassAttributes,
  InstanceType extends BaseClass<Attributes>
> = {
  new (attributes: Attributes): InstanceType
}

// Constructor uses generic A to assign attributes on instances
class BaseClass<A extends IBaseClassAttributes = IBaseClassAttributes> {
  constructor(public attributes: A) {}

  // this returns an instance of type ThisClass
  public static create<A extends IBaseClassAttributes, T extends BaseClass<A>>(
    this: ThisClass<A, T>,
    attributes: A
  ): T {
    // Perform db creation actions here
    return new this(attributes)
  }

  // Note that read function is a place where typechecking could fail you if db
  //   return value does not match
  public static read<A extends IBaseClassAttributes>(id: string): A | null {
    // Perform db retrieval here assign to variable
    const dbReturnValue = {} as A | null
    return dbReturnValue
  }
}

interface IExtendingClassAttributes extends IBaseClassAttributes {
  bar: number
}

// Extend the BaseClass with the extending attributes interface
class ExtendingClass extends BaseClass<IExtendingClassAttributes> {}

// BaseClass
const bc: BaseClass = BaseClass.create({ foo: '' })
const bca: IBaseClassAttributes = BaseClass.read('a') as IBaseClassAttributes
console.log(bc.attributes.foo)
console.log(bca.foo)

// ExtendingClass
// Note that the create and read methods do not have to be overriden,
//  but typechecking still works as expected here
const ec: ExtendingClass = ExtendingClass.create({ foo: 'bar', bar: 0 })
const eca: IExtendingClassAttributes = ExtendingClass.read(
  'a'
) as IExtendingClassAttributes
console.log(ec.attributes.foo)
console.log(ec.attributes.bar)
console.log(eca.foo)
console.log(eca.bar)

您可以轻松地将此模式扩展到剩余的 CRUD 操作。

在@Jack-Barry 的示例中,我尝试让read函数返回该类的实例。 我希望以下工作:

class BaseClass<A extends IBaseClassAttributes = IBaseClassAttributes> {

  public static read<A extends IBaseClassAttributes, T extends BaseClass<A>>(
    this: ThisClass<A, T>,
    id: string,
  ): T | null {
    // Perform db retrieval here assign to variable
    const dbReturnValue = {} as A | null
    if (dbReturnValue === null) {
      return null;
    }
    return this.create(dbReturnValue);
  }
}

而是得到错误Property 'create' does not exist on type 'ThisClass<A, T>'.有人有解决方法吗?

@everhardt由于create方法是static它需要在 this 的this class上调用,而不是实例。 也就是说,对于如何动态访问仍然提供所需类型检查的class函数,我真的没有一个好的解决方法。

我能得到的最接近的不是特别优雅,并且无法使用 BaseClass.create 中的现有type BaseClass.create ,因此它们很容易变得不同步:

return (this.constructor as unknown as { create: (attributes: A) => T }).create(dbReturnValue)

没有_测试过这个。

@Jack-Barry 然后你得到this.constructor.create is not a function

这确实是有道理的:Typescript 将this解释为实例,但对于最终的 javascript 解析器, this是类,因为函数是静态的。

也许不是 Jack-Barry 示例最受启发的扩展,但在这个Playground Link中,您可以看到我是如何解决它的。

症结所在:

  • ThisClass类型应该描述 BaseClass 的方法(静态的和实例上的)想要与多态“this”一起使用的所有静态属性和方法。
  • 在实例方法中使用静态属性或方法时,请使用(this.constructor as ThisClass<A, this>)

这是双重工作,因为您必须在类和ThisClass类型上定义静态方法和属性的类型,但对我来说它有效。

编辑:修复了操场链接

PHP 很久以前就修复了这个问题。 自己。 静止的。 这。

大家好,现在是 2020 年,有 _this_ 问题。 😛

class Animal { 
  static create() { 
    return new this()
  }
}
class Bunny extends Animal {}
const bugs = Bunny.create() // const bugs: Animal

我希望bugsBunny的一个实例。

目前的解决方法是什么? 我已经尝试了这个问题的所有方法,似乎没有任何问题可以解决。

更新:这有效,错过了this参数定义。

class Animal { 
  static create<T extends typeof Animal>(this: T): InstanceType<T> { 
    return (new this()) as InstanceType<T>
  }
}
class Bunny extends Animal {}
const bugs = Bunny.create() // const bugs: Bunny

问题在于 getter 和 setter 不能有类型参数。

这也太冗长了。

我正在处理类似的问题。 要实现“多态内部类”,我能找到的最好的方法如下:

class BaseClass {
  static InnerClass = class BaseInnerClass {};

  static createInnerClass<T extends typeof BaseClass>(this: T) {
    return new this.InnerClass() as InstanceType<T['InnerClass']>;
  }
}

class SubClass extends BaseClass {
  static InnerClass = class SubInnerClass extends BaseClass.InnerClass {};
}

const baseInnerClass = BaseClass.createInnerClass(); // => BaseInnerClass

const subInnerClass = SubClass.createInnerClass(); // => SubInnerClass

它似乎工作正常,但在我看来createInnerClass()的输入太冗长了,我的代码库中会有很多这样的。 有人对如何简化此代码有任何想法吗?

这种方法的问题在于它不适用于静态 getter 和 setter。

为静态属性实现self.怎么样? 4年前我遇到了这个问题,我又回到了同样的问题。

@sudomaxime这超出了这个问题的范围,并且与TypeScript设计目标背道而驰,只要ECMAScript本身不支持self.作为类中this.constructor.的别名,这不太可能鉴于self是一个通用变量名,就会发生这种情况。

@abdullah-kasim 的解决方案似乎按描述工作,但我无法让它与泛型类一起使用:

class Animal<A> {
  public thing?: A;

  static create<T extends typeof Animal>(this: T): InstanceType<T> {
    return new this() as InstanceType<T>;
  }
}

type Foo = {};
class Bunny extends Animal<Foo> {}

const bunny = Bunny.create(); // typeof bunny is Animal<unknown>
bunny.thing;                  // unknown :(

   __     __
  /_/|   |\_\  
   |U|___|U|
   |       |
   | ,   , |
  (  = Y =  )
   |   `  |
  /|       |\
  \| |   | |/
 (_|_|___|_|_)
   '"'   '"'

我会很感激任何关于这是否可行的想法,因为我已经对它进行了一些修改,但没有运气。

@stevehanson这对你有用吗?

class Animal<A> {
  public thing?: A;

  static create<T>(this: new () => T): T {
    return new this() as T;
  }
}

type Foo = {asd: 123};
class Bunny extends Animal<Foo> {
    public hiya: string = "hi there"
}

const bunny = Bunny.create()


bunny.thing

const test = bunny.thing?.asd

const hiya = bunny.hiya

在打字稿操场上对此进行了测试。

受此链接启发:
https://selleo.com/til/posts/gll9bsvjcj-generic-with-class-as-argument-and-returning-instance

而且我不确定它是如何设法暗示 T 是类本身。 编辑:啊,我记得,它设法暗示 T 因为静默this参数将始终传递类本身。

@abdullah-kasim 这行得通! 哇谢谢你! 对于我的具体情况,我的构造函数接受一个参数,所以这对我来说是这样的,以防它帮助其他人:

  static define<C, T, F = any, I = any>(
    this: new (generator: GeneratorFn<T, F, I>) => C,
    generator: GeneratorFn<T, F, I>,
  ): C {
    return new this(generator);
  }

这里的一些解决方案不适用于静态 getter,因为我无法传递任何参数:

鉴于下面的示例,我如何将default静态 getter 移动到父类?

class Letters {
  alpha: string = 'alpha'
  beta?: string
  gamma?: string
  static get default () {
    return new this()
  }
}

const x = Letters.default.alpha;

如果我们考虑语法改进,这可能是值得考虑的事情。

可能相关:我很难在@abdullah-kasim 的示例中添加另一层抽象。 本质上,我希望能够将Animal变成一个接口,并允许Bunny定义自己的静态create()

这是一个实际的例子(请原谅我放弃了兔子的比喻!)。 我希望能够定义几种不同的文档类型,并且每个都应该能够定义一个静态工厂方法,将字符串转换为文档实例。 我想强制一个文档类型必须只为自己定义一个解析器,而不是为其他文档类型。

(下面示例中的类型不太正确——希望我要完成的工作足够清楚)

interface ParseableDoc {
    parse<T>(this: new () => T, serialized:string): T|null;
}

interface Doc {
    getMetadata():string;
}

// EXPECT: no error!
const MarkdownDoc:ParseableDoc = class implements Doc {
    constructor(private meta:string){ };
    getMetadata():string { return this.meta; };

    static parse(serialized:string):typeof MarkdownDoc | null {
        // do something specific
        return null;
    }
}

// EXPECT: type error, since class defines no static parse()
const MissingParseDoc:ParseableDoc = class implements Doc {
    constructor(private meta:string){ };
    getMetadata():string { return this.meta; };
}

// EXPECT: type error, since parse() should return a MismatchedDoc
const MismatchDoc: ParseableDoc = class implements Doc {
    constructor(private meta:string){ };
    getMetadata():string { return this.meta; };

    static parse(serialized:string):typeof MismatchDoc | null {
        // do something specific
        return null;
    }
}

(我想)我希望能够写出这样的东西:

interface ParseableDoc {
    getMetadata():string;
    static parse<T extends ParseableDoc>(this: new () => T, serialized:string): T|null;
}

class MarkdownDoc implements ParseableDoc {
    getMetadata():string { return ""; }

    static parse(serialized:string):MarkdownDoc|null {
        // do something specific
        return null;
    }
}

知道这里是否也可以解决问题吗?

编辑:可能的解决方法

StackOverflow 上的另一个用例
用于设计通用查找表类。

// Generic part
abstract class Table<T extends Model> {
    instances: Map<number, T> = new Map();
}
abstract class Model {
    constructor(
        public readonly id: number,
        public table: Table<this>  // Error
    ) { 
        table.instances.set(id, this);
    }
}

// Example: a table of Person objects
class Person extends Model {
    constructor(
        id: number,
        table: Table<this>,  // Error
        public name: string
    ) {
        super(id, table);
    }
}
class PersonTable extends Table<Person> {}

const personTable = new PersonTable();
const person = new Person(0, personTable, 'John Doe');

// Note: the idea of using `this` as generic type argument is to guarantee
// that other models cannot be added to a table of persons, e.g. this would fail:
//     class SomeModel extends Model { prop = 0; }
//     const someModel = new SomeModel(1, person.table);
//                                        ^^^^^^^^^^^^

@Think7于 2016 年 5 月 29 日发表评论
😰 不敢相信这仍然没有修复/添加.....

哈哈
2020年来了!

这是荒谬和疯狂的。 现在是 2020 年。4 年后,为什么还没有解决这个问题?

为什么不实施类似的东西

class Model {
  static create(){
     return new static()
  }
  //or
  static create(): this {
     return new this()
  }
}

class User extends Model {
  //...
}

let user = new User.create() // type === User
此页面是否有帮助?
0 / 5 - 0 等级

相关问题

bgrieder picture bgrieder  ·  3评论

Zlatkovsky picture Zlatkovsky  ·  3评论

MartynasZilinskas picture MartynasZilinskas  ·  3评论

dlaberge picture dlaberge  ·  3评论

DanielRosenwasser picture DanielRosenwasser  ·  3评论