Typescript: Polymorphes „this“ für statische Mitglieder

Erstellt am 1. Dez. 2015  ·  150Kommentare  ·  Quelle: microsoft/TypeScript

Beim Versuch, ein ziemlich einfaches, aber polymorphes Modellsystem im aktiven Datensatzstil zu implementieren, stoßen wir auf Probleme mit dem Typsystem, das this nicht respektiert, wenn es in Verbindung mit einem Konstruktor oder einer Vorlage/Generika verwendet wird.

Ich habe hier schon einmal darüber gepostet, #5493, und #5492 scheint dieses Verhalten auch zu erwähnen.

Und hier ist ein SO-Beitrag, den ich gemacht habe:
http://stackoverflow.com/questions/33443793/create-a-generic-factory-in-typescript-unsolved

Ich habe mein Beispiel aus #5493 zur weiteren Diskussion in dieses Ticket recycelt. Ich wollte ein offenes Ticket, das den Wunsch nach so etwas und zur Diskussion darstellt, aber die anderen beiden sind geschlossen.

Hier ist ein Beispiel, das ein Modell Factory skizziert, das Modelle produziert. Wenn Sie das BaseModel anpassen möchten, das von $# Factory #$ zurückkommt, sollten Sie es überschreiben können. Dies schlägt jedoch fehl, da this nicht in einem statischen Member verwendet werden kann.

// 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();

Vielleicht ist dieses Problem albern und es gibt eine einfache Problemumgehung. Ich freue mich über Kommentare dazu, wie ein solches Muster erreicht werden kann.

In Discussion Suggestion

Hilfreichster Kommentar

Gibt es einen Grund, warum dieses Thema geschlossen wurde?

Die Tatsache, dass polymorph dies auf Statik nicht funktioniert, macht dieses Feature meiner Meinung nach zum DOA. Ich habe bisher noch nie polymorphes für Instanzmitglieder benötigt, aber ich habe es alle paar Wochen für Statik benötigt, da das System zur Handhabung von Statik bereits in den frühen Tagen fertiggestellt wurde. Ich war überglücklich, als dieses Feature angekündigt wurde, und dann enttäuscht, als mir klar wurde, dass es nur bei Instanzmitgliedern funktioniert.

Der Anwendungsfall ist sehr einfach und sehr häufig. Betrachten Sie eine einfache Factory-Methode:

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

class Bunny extends Animal
{
    hop()
    {
    }
}

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

An diesem Punkt habe ich entweder auf hässliches Casting zurückgegriffen oder static create() -Methoden in jedem Erben verstreut. Das Fehlen dieser Funktion scheint ein ziemlich großes Vollständigkeitsloch in der Sprache zu sein.

Alle 150 Kommentare

Die Größe und Form meines Bootes ist ziemlich ähnlich! Ahoi!

Eine Fabrikmethode in einer Oberklasse gibt neue Instanzen ihrer Unterklassen zurück. Die Funktionalität meines Codes funktioniert, erfordert jedoch, dass ich den Rückgabetyp umwandele:

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.

Ich bin heute auch auf dieses Problem gestoßen!

Eine Fixup-Lösung besteht darin, den untergeordneten Typ als generische Erweiterung der Basisklasse zu übergeben. Dies ist eine Lösung, die ich vorerst anwende:

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>();

Gibt es einen Grund, warum dieses Thema geschlossen wurde?

Die Tatsache, dass polymorph dies auf Statik nicht funktioniert, macht dieses Feature meiner Meinung nach zum DOA. Ich habe bisher noch nie polymorphes für Instanzmitglieder benötigt, aber ich habe es alle paar Wochen für Statik benötigt, da das System zur Handhabung von Statik bereits in den frühen Tagen fertiggestellt wurde. Ich war überglücklich, als dieses Feature angekündigt wurde, und dann enttäuscht, als mir klar wurde, dass es nur bei Instanzmitgliedern funktioniert.

Der Anwendungsfall ist sehr einfach und sehr häufig. Betrachten Sie eine einfache Factory-Methode:

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

class Bunny extends Animal
{
    hop()
    {
    }
}

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

An diesem Punkt habe ich entweder auf hässliches Casting zurückgegriffen oder static create() -Methoden in jedem Erben verstreut. Das Fehlen dieser Funktion scheint ein ziemlich großes Vollständigkeitsloch in der Sprache zu sein.

@paul-go das Thema ist nicht geschlossen... ?

@paul-go Ich war auch frustriert über dieses Problem, aber das Folgende ist die zuverlässigste Problemumgehung, die ich gefunden habe. Jede Animal-Unterklasse müsste super.create() aufrufen und das Ergebnis einfach in ihren Typ umwandeln. Keine große Sache und es ist ein Einzeiler, der leicht entfernt werden kann, wenn dieser hinzugefügt wird.

Der Compiler, Intellisense und vor allem der Hase sind alle glücklich.

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 Oops ... aus irgendeinem Grund habe ich das mit # 5862 verwechselt ... sorry für die Streitaxt-Aggression :-)

@ Think7 Yep ... daher der "Rückgriff auf hässliches Casting oder Verunreinigen statischer create () - Methoden in jedem Erben". Es ist jedoch ziemlich schwierig, wenn Sie ein Bibliotheksentwickler sind und Endbenutzer nicht wirklich zwingen können, eine Reihe von typisierten statischen Methoden in den Klassen zu implementieren, die sie von Ihnen erben.

Gesetz. Habe alles unter deinem Code total verpasst :D

Meh war es wert, muss einen Hasen zeichnen.

:+1: Hase

:kaninchen: :herz:

+1, würde das auf jeden Fall gerne sehen

Gab es Diskussionsupdates zu diesem Thema?

Es bleibt in unserem enormen Vorschlagsbestand.

Javascript verhält sich in einem solchen Muster bereits korrekt. Wenn TS auch folgen könnte, würde uns das eine Menge Boilerplate/Extra-Code ersparen. Das "Modellmuster" ist ziemlich normal, ich würde erwarten, dass TS so funktioniert wie JS.

Ich würde diese Funktion aus den gleichen "CRUD-Modell"-Gründen wie alle anderen auch sehr mögen. Ich brauche es eher für statische Methoden als für Instanzmethoden.

Dies würde eine ordentliche Lösung für das in #8164 beschriebene Problem bieten.

Es ist gut, dass es „Lösungen“ mit Überschreibungen und Generika gibt, aber sie lösen hier nicht wirklich etwas – der ganze Zweck dieser Funktion besteht darin, solche Überschreibungen / Umwandlungen zu vermeiden und Konsistenz mit der Rückgabe this zu schaffen Typ wird in Instanzmethoden behandelt.

Ich arbeite an den Typisierungen für Sequelize 4.0 und es verwendet einen Ansatz, bei dem Sie eine Klasse Model ableiten. Diese Modellklasse hat unzählige statische Methoden wie findById() usw., die natürlich kein Model zurückgeben, sondern Ihre Unterklasse, im statischen Kontext auch this genannt:

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

Dies ist derzeit nicht möglich. Sequelize ist _das_ ORM für Node und es ist traurig, dass es nicht getippt werden kann. Brauche diese Funktion wirklich. Die einzige Möglichkeit besteht darin, jedes Mal, wenn Sie eine dieser Funktionen aufrufen, zu casten oder jede einzelne von ihnen zu überschreiben, den Rückgabetyp anzupassen und nichts anderes zu tun, als super.method() aufzurufen.

Ein weiterer Zusammenhang besteht darin, dass statische Member nicht auf generische Typargumente verweisen können - einige der Methoden nehmen ein Objektliteral von Attributen für das Modell, die über ein Typargument typisiert werden könnten, aber sie sind nur für Instanzmember verfügbar.

😰 Kann nicht glauben, dass das immer noch nicht behoben / hinzugefügt wurde.....

Das könnten wir gut gebrauchen:

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

declare class UIButton extends NSObject {
}

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

Hier ist ein Anwendungsfall, den ich gerne hätte (migriert von https://github.com/Microsoft/TypeScript/issues/9775, den ich dafür geschlossen habe)

Derzeit können die Parameter eines Konstruktors den Typ this nicht verwenden:

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

Erwartet: Dies ist für Konstruktorparameter verfügbar.

Ein Problem, das gelöst werden soll:

  • in der Lage sein, meine Fluent-API entweder um sich selbst oder um eine andere Fluent-API herum aufzubauen, die denselben Code wiederverwendet:
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();

Zukunftssichere Problemumgehung:

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()

Wenn TypeScript this für statische Mitglieder unterstützte, lautet der neue Benutzercode auf diese Weise Baz.create() anstelle von Baz.create<Baz>() , während der alte Benutzercode problemlos funktioniert. :Lächeln:

Das ist wirklich nötig! insbesondere für DAO, die über statische Methoden verfügen, die die Instanz zurückgeben. Die meisten von ihnen sind auf einem Basis-DAO definiert, wie (Speichern, Abrufen, Aktualisieren usw.)

Ich kann sagen, dass es einige Verwirrung stiften könnte, wenn this in einer statischen Methode in die Klasse aufgelöst wird, in der es sich befindet, und nicht in den Typ der Klasse (dh: typeof ).

In echtem JS führt der Aufruf einer statischen Methode für einen Typ dazu, dass this innerhalb der Funktion die Klasse und keine Instanz davon ist ... also inkonsistent.

In der Intuition der Menschen denke ich jedoch, dass das erste, was auftaucht, wenn der Rückgabetyp this für eine statische Methode angezeigt wird, der Instanztyp ist ...

@shlomiassaf Es wäre nicht inkonsistent. Wenn Sie eine Klasse als Rückgabetyp für eine Funktion wie User angeben, ist der Rückgabetyp eine Instanz des Benutzers. Genauso, wenn Sie einen Rückgabetyp von this für eine statische Methode definieren, ist der Rückgabetyp eine Instanz von this (eine Instanz der Klasse). Mit typeof this kann dann eine Methode modelliert werden, die die Klasse selbst zurückgibt.

@felixfbecker , das ist absolut eine Sache des Standpunkts, so entscheiden Sie sich, es zu betrachten.

Lassen Sie uns untersuchen, was in JS passiert, damit wir auf Logik schließen können:

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

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

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

Nun, this in echter Laufzeit ist der begrenzte Kontext der Funktion, genau das passiert in fließenden API-ähnlichen Schnittstellen und die Polymorphie dieser Funktion hilft, indem sie den richtigen Typ zurückgibt, der nur mit der Funktionsweise von JS übereinstimmt. Eine Basisklasse, die this zurückgibt, gibt die Instanz zurück, die von der abgeleiteten Klasse erstellt wurde. Das Feature ist eigentlich ein Fix.

Um zusammenzufassen:
Von einer Methode, die this , die als Teil des Prototyps (Instanz) definiert ist, wird erwartet, dass sie die Instanz zurückgibt.

Wenn Sie mit dieser Logik fortfahren, wird erwartet, dass eine Methode, die this , die als Teil des Klassentyps (Prototyp) definiert ist, den begrenzten Kontext zurückgibt, der der Klassentyp ist.

Nochmals, keine Voreingenommenheit, keine Meinung, einfache Fakten.

Intuitionstechnisch würde ich mich wohl fühlen, wenn this von einer statischen Funktion zurückgegeben wird, die die Instanz darstellt, da sie innerhalb des Typs definiert ist, aber das bin ich. Andere denken vielleicht anders und wir können ihnen nicht sagen, dass sie falsch liegen.

Das Problem ist, dass es möglich sein muss, sowohl eine statische Methode, die eine Klasseninstanz zurückgibt, als auch eine statische Methode, die die Klasse selbst zurückgibt (typeof this), zu typisieren. Ihre Logik ist aus JS-Perspektive sinnvoll, aber wir sprechen hier von Rückgabetypen, und die Verwendung einer Klasse als Rückgabetyp (hier das) in TypeScript und jeder anderen Sprache bedeutet immer die Instanz der Klasse. Um die eigentliche Klasse zurückzugeben, haben wir den typeof-Operator.

@felixfbecker Das wirft ein weiteres Problem auf!

Wenn der Typ this der Instanztyp ist, unterscheidet er sich von dem, worauf sich das Schlüsselwort this im Hauptteil der statischen Methode bezieht. return this ergibt einen Funktionsrückgabetyp von typeof this , was total seltsam ist!

Nein, ist es nicht. Wenn Sie eine Methode wie definieren

getUser(): User {
  ...
}

Sie erwarten, dass Sie eine Benutzer-_Instanz_ zurückbekommen, nicht die Benutzerklasse (dafür ist typeof ). So funktioniert es in jeder getippten Sprache. Die Typsemantik unterscheidet sich einfach von der Laufzeitsemantik.

Warum nicht die Schlüsselwörter this oder static als Konstruktor in func verwenden, um mit einer untergeordneten Klasse zu manipulieren?

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

Und wenn wir dies mit einem Beispiel in JS vergleichen, werden wir sehen, was es richtig funktioniert:

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

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

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

Sie können den Typ this nicht für eine statische Methode verwenden, ich denke, das ist die gesamte Wurzel des Problems.

@felixfbecker

Bedenken Sie:

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

Diese Typanmerkung ist intuitiv, aber falsch, wenn der Typ this in einer statischen Methode die Klasseninstanz ist. Das Schlüsselwort this hat in einer statischen Methode den Typ typeof this !

Ich bin der Meinung, dass sich this _sollte_ auf den Instanztyp und nicht auf den Klassentyp beziehen, da wir vom Instanztyp ( typeof X ) eine Referenz auf den Klassentyp erhalten können, aber nicht umgekehrt ( instancetypeof X ?)

@xealot okay, warum nicht das Schlüsselwort static statt this verwenden? Aber this im statischen JS-Kontext zeigt immer noch auf einen Konstruktor.

@izatop ja, das generierte Javascript funktioniert (ordnungsgemäß oder nicht ordnungsgemäß). Allerdings geht es hier nicht um Javascript. Die Beschwerde ist nicht, dass die Javascript-Sprache dieses Muster nicht zulässt, sondern dass das Typsystem von Typescript dies nicht zulässt.

Mit diesem Muster können Sie die Typsicherheit nicht aufrechterhalten, da Typescript keine polymorphen this -Typen für statische Elemente zulässt. Dazu gehören Klassenmethoden, die mit dem Schlüsselwort static und constructor definiert sind.

Spielplatz Link

@LPGhatguy , aber du hast meinen vorherigen Kommentar gelesen, oder?! Wenn Sie eine Methode mit dem Rückgabetyp User haben, erwarten Sie return new User(); , nicht return User; . Auf die gleiche Weise sollte ein Rückgabewert von this return new this(); bedeuten. Bei nicht statischen Methoden ist dies anders, aber es ist klar, dass this immer ein Objekt ist und Sie es nicht instanziieren können. Aber am Ende sind wir uns im Grunde alle einig, dass dies wegen typeof die beste Syntax ist und dass diese Funktion in TS dringend benötigt wird.

@xealot Ich verstehe, was TypeScript polymorphes this nicht zulässt, aber ich werde Sie fragen, warum Sie diese Funktion nicht zu TS hinzufügen?

Ich weiß nicht, ob das Folgende für alle Anwendungsfälle dieses Problems funktioniert, aber ich habe herausgefunden, dass die Verwendung des Typs this: in statischen Methoden in Verbindung mit der intelligenten Verwendung des Typrückschlusssystems Spaß macht tippen. Es ist vielleicht nicht besonders lesbar, aber es erfüllt seine Aufgabe, ohne statische Methoden in den Kinderklassen neu definieren zu müssen.

Funktioniert mit [email protected]

Folgendes berücksichtigen ;

// 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 warum wird this als Parameter angegeben?

Dies liegt an https://github.com/Microsoft/TypeScript/issues/3694 , das mit TypeScript 2.0.0 ausgeliefert wurde, das die Definition eines this-Parameters für Funktionen ermöglicht (wodurch die Verwendung von this in ihrem Körper ohne Probleme und um auch eine Fehlbedienung der Funktionen zu verhindern)

Es stellt sich heraus, dass wir sie sowohl für Methoden als auch für statische Methoden verwenden können und dass sie wirklich this Dinge ermöglichen, wie z (IModell) zu dem verwendeten (Hund oder Katze). Es "errät" dann den richtigen Typ für T und verwendet ihn, wodurch unsere Funktion korrekt eingegeben wird.

(Beachten Sie, dass this ein _spezieller_ Parameter ist, der vom Typoskript-Compiler verstanden wird, er fügt der Funktion keinen weiteren hinzu.)

Ich dachte, die Syntax wäre so etwas wie <this extends Whatever> . Das würde also immer noch funktionieren, wenn create() andere Parameter hat?

Absolut

Ich wollte auch darauf hinweisen, dass dies für eingebaute Typen wichtig ist.
Wenn Sie beispielsweise Array ableiten, gibt die Methode from() der Unterklasse eine Instanz der Unterklasse zurück, nicht 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

Irgendwelche Updates zu diesem Thema? Diese Funktion würde die Erfahrung meines Teams mit TypeScript erheblich verbessern.

Bitte beachten Sie, dass dies ziemlich kritisch ist, da es sich um ein häufig verwendetes Muster in ORMs handelt und sich eine Menge Code ganz anders verhält, als der Compiler und das Typsystem denken, dass es sollte.

Ich würde einige Gegenargumente gegen Gegenargumente erheben.

Wie @shlomiassaf betonte, wird das Zulassen this in statischen Membern zu Verwirrung führen, da this in der statischen Methode und der Instanzmethode etwas anderes bedeutet. Darüber hinaus haben this in der Typannotation und this im Methodentext unterschiedliche Semantik. Wir können diese Verwirrung vermeiden, indem wir ein neues Schlüsselwort einführen, aber die Kosten für ein neues Schlüsselwort sind hoch.

Und es gibt eine einfache Problemumgehung im aktuellen TypeScript. @ceymard hat das schon gezeigt . Und ich würde seinen Ansatz bevorzugen, weil er die Laufzeitsemantik der statischen Methode erfasst. Betrachten Sie diesen Code.

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

Wenn dies polymorph unterstützt wird, sollte der Compiler einen Fehler in class B extends A melden. Aber das ist eine bahnbrechende Änderung.

@HerringtonDarkholme Ich sehe die Verwirrung nicht, der Rückgabetyp von this ist genau das, was ich von return new this() erwarten würde. Möglicherweise könnten wir dafür self verwenden, aber die Einführung eines anderen Schlüsselworts (anders als das zur Erzeugung der Ausgabe verwendete) scheint mir verwirrender zu sein. Wir verwenden es bereits als Rückgabetyp, das einzige (aber große) Problem ist, dass es in statischen Membern nicht unterstützt wird.

Der Compiler sollte von Programmierern keine Problemumgehungen verlangen, TypeScript soll ein "erweitertes JavaScript" sein und das stimmt in diesem Fall nicht, Sie müssen eine komplexe Problemumgehung für Ihren Code durchführen, um nicht Millionen von Fehlern zu erzeugen. Es ist schön, dass wir es in der aktuellen Version irgendwie erreichen können, aber das bedeutet nicht, dass es in Ordnung ist und keine Korrektur benötigt.

Warum unterscheidet sich der Rückgabetyp von this dann vom Typ von this im Methodenkörper?
Warum schlägt dieser Code fehl? Wie erklärt man es einem Neuankömmling?

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

Warum würde eine Obermenge von JavaScript dies nicht akzeptieren?

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

abstract class B extends A {}

Stimmt, dann müssen wir vielleicht ein anderes Schlüsselwort einführen. Könnte self oder vielleicht instance sein

@HerringtonDarkholme So erkläre ich es einem Neuling.
Wenn Sie das tun

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

der Rückgabetyp von B bedeutet, dass Sie eine Instanz von B zurückgeben müssen. Wenn Sie die Klasse selbst zurückgeben möchten, müssen Sie verwenden

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

Nun, genau wie in JavaScript, bezieht sich this in statischen Membern auf die Klasse. Wenn Sie also in einer statischen Methode den Rückgabetyp this verwenden, müssen Sie eine Instanz der Klasse zurückgeben:

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

Wenn Sie die Klasse selbst zurückgeben möchten, müssen Sie typeof this verwenden:

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

genau so funktionieren Typen schon seit jeher in TS. Wenn Sie mit einer Klasse eingeben, erwartet TS eine Instanz. Wenn Sie für den Klassentyp selbst eingeben möchten, müssen Sie typeof verwenden.

Dann würde ich fragen, warum this im Methodenkörper nicht dieselbe Bedeutung hat. Wenn this in der Typanmerkung der Instanzmethode dieselbe Bedeutung wie this im entsprechenden Methodentext hat, warum ist es dann nicht dasselbe für die statische Methode?

Wir stehen vor einem Dilemma und es gibt drei Möglichkeiten:

  1. make this bedeutet verschiedene Dinge in verschiedenen Kontexten (Verwirrung)
  2. Führen Sie ein neues Schlüsselwort wie self oder static ein
  3. Lassen Sie den Programmierer mehr seltsame Anmerkungen schreiben (beeinflussen einige Benutzer)

Ich bevorzuge den dritten Ansatz, weil er die Laufzeitsemantik der statischen Methodendefinition widerspiegelt.
Es erkennt auch Fehler auf der Website verwenden, anstatt die Website zu definieren, das heißt, wie in diesem Kommentar wird der Fehler in B.create gemeldet, nicht in class B extends A . Use site error ist in diesem Fall genauer. (Stellen Sie sich vor, Sie deklarieren eine statische Methode, die auf this verweist, und deklarieren dann eine abstrakte Unterklasse).

Und was am wichtigsten ist, es erfordert keinen Sprachvorschlag für neue Funktionen.

In der Tat unterscheidet sich die Präferenz von Personen. Aber zumindest möchte ich einen detaillierteren Vorschlag wie diesen sehen. Es gibt viele Probleme, die gelöst werden müssen, wie z. B. die Zuweisung abstrakter Klassen, Unterklassen-inkompatible Konstruktorsignaturen und Strategien für Fehlerberichte.

@HerringtonDarkholme Denn im Fall von Instanzmethoden ist die Laufzeit this eine Objektinstanz und keine Klasse, sodass kein Platz für Verwirrung besteht. Es ist klar, dass der Typ this buchstäblich die Laufzeit this ist, es gibt keine andere Möglichkeit. Im statischen Fall verhält es sich wie jede andere Klassenannotation in jeder objektorientierten Programmiersprache: Kommentieren Sie es mit einer Klasse, und Sie erwarten eine Instanz zurück. Plus im TS-Fall, weil in JavaScript auch Klassen Objekte sind, geben Sie mit typeof eine Klasse ein und Sie erhalten die wörtliche Klasse zurück.

Ich meine, es wäre wahrscheinlich konsequenter gewesen, als sie damals den Rückgabetyp this implementierten, um auch über den statischen Fall nachzudenken und stattdessen von Benutzern zu verlangen, immer typeof this für Instanzmethoden zu schreiben. Aber diese Entscheidung wurde damals getroffen, also müssen wir jetzt damit arbeiten, und wie gesagt, es lässt keinen Raum für Verwirrung, da this in Instanzmethoden keinen anderen Typ erzeugt (im Gegensatz zu Klassen, who einen statischen Typ und einen Instanztyp haben), also ist es in diesem Fall anders und wird niemanden verwirren.

OK, nehmen wir einfach an, dass niemand durch this verwirrt wird. Wie sieht es mit der Zuweisung von abstrakten Klassen aus?

Die Statik von Methoden ist etwas willkürlich. Jede Funktion kann Eigenschaften und Methoden haben. Wenn es auch mit new aufgerufen werden kann, nennen wir diese Eigenschaften und Methoden static . Jetzt wurde diese Konvention fest in ECMAScript verankert.

@HerringtonDarkholme Sie haben zweifellos Recht, dass die Verwendung von this Verwirrung stiften würde. Da jedoch nichts Falsches an der Verwendung this ist, würde ich sagen, dass es völlig in Ordnung ist, insbesondere wenn Sie Ihre scharfsinnige Kosten-Nutzen-Analyse der verschiedenen Alternativen berücksichtigen. Ich denke, this ist die am wenigsten schlechte Alternative.

Ich habe gerade versucht, diesen Thread von meinem ursprünglichen Beitrag nachzuholen, ich habe das Gefühl, dass dies vielleicht ein wenig aus der Spur geraten ist.

Zur Verdeutlichung: Die Annotation des Typs this soll polymorph sein, wenn sie im Konstruktor verwendet wird, insbesondere als Generic/Template. In Bezug auf @shlomiassaf und @HerringtonDarkholme glaube ich, dass das Beispiel mit static -Methoden verwirrend sein kann und nicht die Absicht dieses Problems ist.

Obwohl es nicht oft als solcher angesehen wird, ist der Konstruktor einer Klasse statisch. Das Beispiel (das ich mit klareren Kommentaren erneut veröffentlichen werde) deklariert nicht this für eine statische Methode, sondern deklariert stattdessen this für die zukünftige Verwendung über ein Generikum in der Typanmerkung einer statischen Methode .

Der Unterschied besteht darin, dass ich this nicht sofort auf einer statischen Methode berechnen lassen möchte, sondern in Zukunft auf einer dynamischen.

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

Für alle, die denken, dass dies verwirrend ist, nun, es ist nicht sehr einfach, da stimme ich zu. Die Verwendung von this im Konstruktor BaseModel ist jedoch nicht wirklich eine statische Verwendung, sondern eine verzögerte Verwendung, die später berechnet wird. Statische Methoden (einschließlich des Konstruktors) funktionieren jedoch nicht auf diese Weise.

Ich denke, zur Laufzeit funktioniert das alles wie erwartet. Das Typensystem ist jedoch nicht in der Lage, dieses spezielle Muster zu modellieren. Die Unfähigkeit von Typoskript, ein Javascript-Muster zu modellieren, ist die Grundlage dafür, warum dieses Problem offen ist.

Entschuldigung, wenn dies ein verwirrender Kommentar ist, der in Eile geschrieben wurde.

@xealot Ich verstehe deinen Standpunkt, aber andere Probleme, die speziell polymorphe this für statische Methoden vorschlugen, wurden als Duplikat von diesem geschlossen. Ich gehe davon aus, dass der Fix in TS beide Anwendungsfälle ermöglichen würde.

Was ist Ihr konkreter Anwendungsfall? Vielleicht reicht die ´diese´ Lösung.

Ich verwende es erfolgreich in einer benutzerdefinierten ORM-Bibliothek
Le jeu. 20. Okt. 2016 à 19:15, Tom Marius [email protected] a
Schrift :

Bitte beachten Sie, dass dies ziemlich kritisch ist, da es häufig verwendet wird
Muster in ORMs und viel Code verhält sich ganz anders als die
Compiler und Typsystem denken, dass es sollte.


Sie erhalten dies, weil Sie erwähnt wurden.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/Microsoft/TypeScript/issues/5863#issuecomment -255169194,
oder den Thread stumm schalten
https://github.com/notifications/unsubscribe-auth/AAtAoRHc45B386xdNhuCLB8iW6i82e7Uks5q16GzgaJpZM4GsmOH
.

Danke an @ceymard und @HerringtonDarkholme.

static create<T extends A>(this: {new (): T}) { return new this(); } hat es mir angetan 👍

Es ist auf den ersten Blick obskurer als static create(): this { return new this(); } , aber zumindest ist es richtig! Es erfasst genau den Typ von this innerhalb der statischen Methode.

Ich hoffe, dass dies irgendwie priorisiert werden kann, auch wenn es nicht am meisten positiv bewertet oder kommentiert wird usw. Es ist sehr ärgerlich, das Typargument verwenden zu müssen, um den richtigen Typ zu erhalten, zum Beispiel:

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

Wenn es praktisch möglich ist, einfach Folgendes zu tun:

let u = User.create();

Und es ist unvermeidlich, dass sich statisch manchmal auf Instanztypen bezieht.

Ich hatte eine andere Idee in Bezug auf die Diskussion darüber, ob der Typ this in statischen Membern der Typ der statischen Seite der Klasse oder der Instanzseite sein sollte. this könnte die statische Seite sein, es würde übereinstimmen, was in Instanzmitgliedern getan wird und wie das Laufzeitverhalten ist, und dann könnten Sie den Instanztyp über this.prototype :

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

class User extends Model {}

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

Dies würde der Funktionsweise in JavaScript entsprechen. this ist das Klassenobjekt selbst, und die Instanz befindet sich auf this.prototype . Es handelt sich im Wesentlichen um die gleiche Mechanik wie bei zugeordneten Typen, was this['prototype'] entspricht.

Auf die gleiche Weise könnten Sie sich vorstellen, dass die statische Seite in Instanzmitgliedern über this.constructor verfügbar ist (ich kann mir keinen Anwendungsfall für diesen atm vorstellen).

Ich wollte auch erwähnen, dass keiner der genannten Problemumgehungen/Hacks für die Eingabe des ORM-Modells in Sequelize funktioniert.

  • Die this kann nicht in einem Konstruktor verwendet werden, daher funktioniert sie nicht für diese Signatur:
    ts abstract class Model { constructor(values: Partial<this.prototype>) }
  • Das Übergeben des Typs als generisches Klassenargument funktioniert bei statischen Methoden nicht, da statische Member nicht auf generische Parameter verweisen können
  • Das Übergeben des Typs als generisches Methodenargument funktioniert für Methoden, aber nicht für statische Eigenschaften wie:
    ts abstract class Model { static attributes: { [K in keyof this.prototype]: { type: DataType, field: string, unique: boolean, primaryKey: boolean, autoIncrement: boolean } }; }

+1 für diese Anfrage. mein ORM wird viel sauberer sein.

Hoffentlich sehen wir bald die saubere Lösung

In der Zwischenzeit danke an alle, die Lösungen angeboten haben!

Können wir weitermachen, da es keine Diskussion mehr gibt?

Wo möchten Sie es hinbringen? Dies ist immer noch eine Möglichkeit, in der Typescript Javascript meines Wissens nach nicht sauber modellieren kann und keine andere Lösung gesehen hat, als es einfach nicht zu tun.

Dies ist ein Muss für ORM-Entwickler, da bereits drei beliebte ORM-Besitzer nach dieser Funktion gefragt haben.

@pleerock @xealot Lösungen wurden oben vorgeschlagen:

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();

Ich benutze das ausgiebig. Die Deklarationen der statischen Methoden in Basisklassen sind etwas schwerfällig, aber das ist der Preis, den man zahlen muss, um eine Verzerrung des Typs von this innerhalb statischer Methoden zu vermeiden.

@pleerock

Ich habe ein internes ORM, das das this: -Muster ohne Probleme ausgiebig verwendet.

Ich denke, es besteht keine Notwendigkeit, die Sprache zu überladen, wenn das Feature tatsächlich schon da ist, wenn auch zugegebenermaßen ein wenig verworren. Der Anwendungsfall für eine klarere Syntax ist meiner Meinung nach ziemlich begrenzt und kann zu Inkonsistenzen führen.

Vielleicht könnte es einen Stackoverflow-Thread mit dieser Frage und Lösungen als Referenz geben?

@bjouhier @ceymard Ich habe erklärt, warum alle Workarounds in diesem Thread nicht für Sequelize funktionieren: https://github.com/Microsoft/TypeScript/issues/5863#issuecomment -269463313

Und hier geht es nicht nur um ORMs, sondern auch um die Standardbibliothek: https://github.com/Microsoft/TypeScript/issues/5863#issuecomment -244550725

@felixfbecker Ich habe gerade etwas über den Konstruktor-Anwendungsfall gepostet und ihn gelöscht (ich habe festgestellt, dass TS nicht direkt nach dem Posten Konstruktoren in jeder Unterklasse vorschreibt).

@felixbecker In Bezug auf den Konstruktorfall löse ich ihn, indem ich mich an parameterlose Konstruktoren halte und stattdessen spezialisierte statische Methoden bereitstelle (erstellen, klonen, ...). Deutlicher und einfacher zu tippen.

@bjouhier Das würde bedeuten, dass Sie im Grunde jede einzelne Methode in jeder Modellunterklasse neu deklarieren müssen. Schauen Sie, wie viele es in Sequelize gibt: http://docs.sequelizejs.com/class/lib/model.js~Model.html

Meine Argumentation erfolgt aus einer ganz anderen Perspektive. Es gibt zwar teilweise und etwas unintuitive Problemumgehungen und wir können darüber streiten und uns darüber einigen, ob diese ausreichend sind oder nicht, aber wir sind uns einig, dass dies ein Bereich ist, in dem Typescript Javascript nicht einfach modellieren kann.

Und nach meinem Verständnis sollte Typescript eine Erweiterung von Javascript sein.

@felixfbecker

Das würde bedeuten, dass Sie praktisch jede einzelne Methode in jeder Modellunterklasse neu deklarieren müssen.

Wieso den? Das ist überhaupt nicht meine Erfahrung. Können Sie das Problem mit einem Codebeispiel veranschaulichen?

@bjouhier Ich habe das Problem ausführlich mit Codebeispielen in meinen Kommentaren in diesem Thread illustriert, für den Anfang https://github.com/Microsoft/TypeScript/issues/5863#issuecomment -222348054

Aber sehen Sie sich mein create Beispiel oben an. Die Methode create ist eine statische Methode. Es wird nicht neu deklariert und dennoch korrekt in die Unterklasse eingegeben. Warum sollten die Methoden Model dann neu definiert werden?

@felixfbecker Dein _für den Anfang_ Beispiel:

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 Okay, es scheint also so, als ob die this: { new (): T } -Anmerkungen tatsächlich Typrückschlüsse funktionieren lassen. Was mich wundert, warum dies nicht der Standardtyp ist, den der Compiler verwendet.
Der Workaround funktioniert natürlich nicht für den Konstruktor, da dieser keinen this Parameter haben kann.

Ja, es funktioniert nicht für Konstruktoren, aber Sie können das Problem mit einer kleinen API-Änderung umgehen, indem Sie eine statische create -Methode hinzufügen.

Ich verstehe das, aber dies ist eine JavaScript-Bibliothek, also sprechen wir davon, die vorhandene API in eine Deklaration einzugeben.

Wenn this { new (): T } #$ bedeuten würde, dann wäre this nicht der richtige Typ für this innerhalb der statischen Methode. Zum Beispiel wäre new this() nicht erlaubt. Aus Konsistenzgründen muss this der Typ der Konstruktorfunktion sein, nicht der Typ der Klasseninstanz.

Ich bekomme das Problem mit der Eingabe einer vorhandenen Bibliothek. Wenn Sie keine Kontrolle über diese Bibliothek haben, können Sie trotzdem eine Zwischenbasisklasse ( BaseModel extends Model ) mit einer richtig typisierten create Funktion erstellen und alle Ihre Modelle von BaseModel ableiten.

Wenn Sie auf statische Eigenschaften der Basisklasse zugreifen möchten, können Sie verwenden

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

Aber Sie haben wahrscheinlich einen gültigen Standpunkt zum Konstruktor. Ich sehe keinen triftigen Grund, warum der Compiler Folgendes ablehnen muss:

constructor(values: Partial<this>) {}

Ich stimme @xealot zu, dass es hier Schmerzen gibt. Es wäre so cool, wenn wir schreiben könnten

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

anstatt

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

Aber wir brauchen einen neuen Operator im Schreibsystem ( instanceof ?). Folgendes ist ungültig:

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

TS 2.4 hat diese Workarounds für mich kaputt gemacht:
https://travis-ci.org/types/sequelize/builds/247636686

@sandersn @felixfbecker Ich denke, das ist ein gültiger Fehler. Ich kann es jedoch nicht minimal reproduzieren. Callback-Parameter ist kontravariant.

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

Gibt es eine Chance, dass das irgendwann behoben wird?

Ich bin heute auch darauf gestoßen. Grundsätzlich wollte ich eine Singleton-Basisklasse wie folgt erstellen:

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

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

Die Verwendung wäre etwa so:

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

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

Ich habe ungefähr 10 Variationen davon ausprobiert, mit der oben erwähnten StaticThis -Variante und vielen anderen. Am Ende gibt es nur eine Version, die überhaupt kompilieren würde, aber dann wurde das Ergebnis von getInstance() nur als Object abgeleitet.

Ich habe das Gefühl, dass es viel, viel schwieriger ist, als es sein muss, mit Konstrukten wie diesen zu arbeiten.

Folgende Arbeiten:

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();

Der this as any as typeof Singleton -Teil ist hässlich, aber er weist darauf hin, dass wir das Typsystem täuschen, da _instance im Konstruktor der abgeleiteten Klasse gespeichert werden muss.

Normalerweise wird die Basisklasse in Ihrem Framework vergraben, sodass es nicht viel schadet, wenn ihre Implementierung etwas hässlich ist. Was zählt, ist sauberer Code in abgeleiteten Klassen.

Ich hatte gehofft, in 2.8 Nightlies so etwas tun zu können:

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

Leider kein Glück :(

@bjouhier Codebeispiele funktionieren wie ein Zauber. Aber unterbrechen Sie die automatische Vervollständigung von IntelliSense.

React 16.3.0 wurde veröffentlicht und es scheint unmöglich, die neue getDerivedStateFromProps -Methode richtig einzugeben, da eine statische Methode keine Klassentypparameter referenzieren kann:

(vereinfachtes Beispiel)

class Component<P, S> {

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

    props: P
    state: S
}

Gibt es eine Problemumgehung? (Ich zähle "Eingabe von P und S als PP und SS und hoffe, dass Entwickler diese beiden Typfamilien genau übereinstimmen" nicht als eine :p)

PR: https://github.com/DefinitelyTyped/DefinitelyTyped/pull/24577

@cncolder Ich habe auch das Brechen von Intellisense miterlebt. Ich glaube, es begann mit Typoskript 2.7.

Eine Sache, die ich hier nicht sehe, ist eine Erweiterung des Singleton-Beispiels - es wäre ein gängiges Muster, eine Singleton / Multiton-Klasse mit einem geschützten Konstruktor zu versehen - und dies kann nicht erweitert werden, wenn der Typ wie folgt { new(): T } ist erzwingt einen öffentlichen Konstruktor - der Begriff this oder eine andere hier bereitgestellte Lösung ( instanceof this , typeof this usw.) sollte dieses Problem lösen. Dies ist definitiv möglich, indem ein Fehler im Konstruktor ausgelöst wird, wenn der Singleton seine Erstellung nicht angefordert hat usw., aber das vereitelt den Zweck der Eingabe von Fehlern - um eine Laufzeitlösung zu haben ...

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

Ich weiß nicht, warum Sie ein Singleton in TS mit getInstance() benötigen würden, Sie können einfach ein Objektliteral definieren. Oder wenn Sie unbedingt Klassen verwenden möchten (für private Mitglieder), exportieren Sie nur die Klasseninstanz und nicht die Klasse, oder verwenden Sie einen Klassenausdruck ( export new class ... ).

Dies ist nützlicher für Multiton - und ich glaube, ich habe ein Muster gefunden, das funktioniert, ... obwohl es ohne diesen Typ immer noch ein sehr hackiges Gefühl ist ... stattdessen ist dies notwendig

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

Ich habe auch etwas Code, um den Schlüssel zu speichern, damit er bei der Erstellung in der untergeordneten Klasse zugänglich ist, aber ich habe das der Einfachheit halber entfernt ...

Hallo. Ich stoße auf ein Problem, das polymorphes "this" für den Konstruktor benötigt. Ähnlich wie dieser Kommentar.

Ich möchte eine Klasse erstellen. Die Klasse selbst hat ziemlich viele Eigenschaften, sodass der Konstruktor nicht gut aussieht, wenn alle Eigenschaften dieser Klasse über Parameter des Konstruktors initialisiert werden.

Ich bevorzuge einen Konstruktor, der ein Objekt akzeptiert, um Eigenschaften zu initialisieren. Beispielsweise:

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

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

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

Dann möchte ich die Klasse verlängern. Beispielsweise:

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

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

Es wäre sehr schön, wenn der Typ this auch für den Konstruktor verwendet werden könnte.

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

Daher kann IntelliSense Eigenschaften der untergeordneten Klasse ohne zusätzliche Boilerplate ableiten: z. B. Neudefinition des Konstruktors. Es wird natürlicher aussehen, als eine statische Methode zum Initialisieren der Klasse zu verwenden.

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

Vielen Dank für Ihre Aufmerksamkeit.

Ich bin froh, dass ich diesen Thread gefunden habe. Ich denke ehrlich, dass dies ein sehr ernstes Problem ist und war sehr entmutigt, es auf 3.0 durchzusetzen.

Wenn ich mir alle Lösungen anschaue und die verschiedenen Problemumgehungen betrachte, mit denen ich in den letzten Jahren herumgespielt habe, komme ich zu dem Schluss, dass jede schnelle Lösung , die wir versuchen, einfach scheitern wird, wenn und sobald eine Lösung vorhanden ist. Und bis dahin erfordert jeder Zwang, den wir vornehmen, einfach zu viel manuelle Arbeit, um ihn über das gesamte Projekt hinweg aufrechtzuerhalten. Und nach der Veröffentlichung können Sie die Verbraucher nicht bitten, Ihre eigene Denkweise zu verstehen und warum Sie sich entschieden haben, etwas zu reparieren, das TypeScript ist, wenn sie Ihr Paket einfach in Vanilla konsumieren, aber wie die meisten von uns mit VSCode arbeiten.

Gleichzeitig widerspricht das Schreiben von Code, um das Typsystem glücklich zu machen, wenn die eigentliche Sprache nicht das Problem ist, dem gleichen Grund für die Verwendung von typisierten Funktionen von Anfang an.

Meine Empfehlung an alle ist, zu akzeptieren, dass die Squigglies fehlerhaft sind und wenn möglich //@ts-ignore because my code works as expected zu verwenden.

Wenn Ihr Linter gegen vernünftige menschliche Intelligenz immun ist, ist es an der Zeit, einen zu finden, der seinen Platz kennt.

AKTUALISIEREN :
Ich wollte meinen eigenen Versuch hinzufügen, die statische Inferenz vielleicht sicher zu erweitern. Dieser Ansatz ist streng umgebungsbedingt, da er abgelegen sein soll. Es ist erschöpfend explizit und zwingt Sie, Ihre Augmentationen zu überprüfen und nicht einfach anzunehmen, dass die Vererbung die Arbeit für Sie erledigen wird.

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

Natürlich kann ich nicht garantieren, dass dieses Muster gilt oder dass es jedes Szenario in diesem Thread erfüllen würde, aber im Moment funktioniert es wie erwartet.

Diese Entscheidungen resultieren aus der besorgniserregenden Erkenntnis, dass die eigenen lib-Dateien von TypeScript bis zu diesem Zeitpunkt nur zwei Klassendeklarationen enthalten!

SafeArray

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

VarDatum

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

Heute mal kurz diskutiert. Kernpunkte:

  • Bezieht sich this auf die Instanzseite oder die statische Seite?

    • Auf jeden Fall die statische Seite. Auf die Instanzseite kann über InstanceTypeOf verwiesen werden, aber das Gegenteil ist nicht der Fall. Dadurch bleibt auch die Symmetrie erhalten, dass this in der Signatur den gleichen Typ hat wie this im Hauptteil.

  • Soliditätsprobleme

    • Es gibt keine Garantie dafür, dass eine Konstruktor-Parameterliste einer abgeleiteten Klasse in irgendeiner Beziehung zu ihrer Konstruktor-Parameterliste der Basisklasse steht

    • Klassen brechen diesbezüglich extrem oft die statikseitige Substituierbarkeit

    • Nicht-konstruktseitige Substituierbarkeit wird erzwungen und das ist typischerweise das, woran die Menschen ohnehin interessiert sind

    • Wir erlauben bereits unzuverlässige Aufrufe von new this() in static Methoden

    • Wahrscheinlich interessiert sich niemand von außen für die Konstrukt-Signaturseite des statischen this ?

Bestehende Problemumgehungen:

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();

Insbesondere funktioniert dies nur, wenn die Konstruktsignatur Bar die von Foo ordnungsgemäß ersetzen kann

@RyanCavanaugh Ich habe versucht, eine Erklärung zu finden, woher der Wert für this in Ihrem Beispiel static boo<T extends typeof Foo>(this: T): InstanceType<T> kommt, aber ich verstehe es einfach nicht. Um sicher zu sein, habe ich alles über Generika noch einmal gelesen, aber trotzdem - nein. Wenn ich zum Beispiel this durch clazz ersetze, funktioniert es nicht mehr und der Compiler beschwert sich mit "Expected 1 arguments, but got 0." Da geht also etwas Magie vor sich. Könntest du erklären? Oder weisen Sie mich in der Dokumentation in die richtige Richtung?

Bearbeiten:
Und warum ist es extends typeof Foo und nicht einfach extends Foo ?

@creynders this ist ein spezieller gefälschter Parameter, der für den Kontext der Funktion eingegeben werden kann. Dokumente .

Die Verwendung InstanceType<T> funktioniert nicht, wenn Generika im Spiel sind.

Mein Anwendungsfall sieht in etwa so aus:

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-Playground-Link

@mastermatt oh wow, das ist mir beim Lesen der Dokumente absolut NICHT aufgefallen ... Danke

Wenn ich mir das Beispiel von @RyanCavanaugh anschaue , konnte ich unsere statischen Methoden zum Laufen bringen, aber ich habe auch Instanzmethoden, die sich auf die statischen Methoden beziehen, aber ich kann nicht herausfinden, wie ich dafür die richtige Eingabe bekomme.

Beispiel:

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();

Wenn ich eine Problemumgehung für dieses letzte Problem finden könnte, wäre ich bereit, bis etwas Offizielles kommt.

Bemerkenswert bei diesem Beispiel ist auch, wenn die Unterklasse versucht, eine statische Methode zu überschreiben und dann super aufzurufen, dann ist auch alles verärgert ... Abgesehen von diesen beiden Problemen scheint die Problemumgehung in Ordnung zu sein. Obwohl diese beiden Probleme unseren Code eher blockieren.

IIRC liegt daran, dass dies nicht in Typoskript ausgedrückt werden kann, ohne das Typsystem ein wenig zu biegen. Wie in ; this.constructor ist überhaupt nicht garantiert das, was Sie denken, oder so ähnlich.

In genau diesem Fall würde ich die Instanzmethode boo() this zurückgeben lassen und ein wenig schummeln, wodurch der Compiler gezwungen würde zu akzeptieren, dass ich weiß, was ich tue.

Meine allgemeine Überlegung ist, dass ich möchte, dass die API für den Rest des Codes so einfach wie möglich ist, auch wenn dies manchmal ein wenig Schummeln bedeutet.

Wenn jemand etwas Robusteres hat, würde ich mich freuen

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();

Sie können auch die Schnittstellenroute gehen und so etwas tun;

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();

Das ist auch Betrug, aber vielleicht etwas kontrollierter? Ich kann es nicht wirklich sagen.

Das erste Argument this in der statischen "boo"-Methode wird also speziell behandelt, nicht als reguläres Argument? Irgendwelche Links zu Dokumenten, die dies beschreiben?

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();

Ich habe das gleiche (zumindest ähnliche) Problem.
Ich verwende Vanilla Javascript mit JSDoc (nicht TypeScript), daher kann ich die in diesem Thread vorgeschlagenen Problemumgehungen, die sich um Generika drehen, nicht verwenden/implementieren, aber der Stamm scheint derselbe zu sein.
Ich habe diese Ausgabe geöffnet: #28880

Dieses Problem haben buchstäblich 3 Jahre bereits.
Hat jemand eine passende Problemumgehung gefunden, die für mich funktionieren könnte?

3 Jahre

Die Umgehung von @RyanCavanaugh eignet sich hervorragend für statische Methoden, aber was ist mit einem statischen Accessor?

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

3 Jahre

Okay, du bist halbwegs anständig darin, Datumsangaben zu subtrahieren. Aber können Sie eine Lösung anbieten? ;)

@RyanCavanaugh Dies ist ein Problem, wenn wir den this in Generatoren wie folgt verwenden:

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

Wenn wir Generatoren erstellen, können wir keine Pfeilfunktionen verwenden, um den this zu verwenden. Jetzt müssen wir den Typ this explizit in Unterklassen deklarieren. Ein tatsächlicher Fall sieht folgendermaßen aus:

      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

Wir müssen den Typ this nicht in Unterklassen deklarieren, wenn wir dies in Basisklassen tun können. Der Wert this wird jedoch nicht initialisiert, wenn der Generator synchron in der Oberklasse aufgerufen wird. Ich habe dieses Problem im Code vermieden, aber das ist ein schmutziger Hack. Daher kann dieses Muster ursprünglich nicht mit klassenbasierten Programmiersprachen übereinstimmen.

Nicht die sauberste, aber möglicherweise nützlich für den Zugriff auf die untergeordnete Klasse von einer statischen Methode.

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

class C extends Base {
}

C.foo();

Ich bin mir zwar nicht sicher, ob das Problem, mit dem ich derzeit konfrontiert bin, darauf zurückzuführen ist, aber nach einem kurzen Blick auf dieses Problem scheint es wahrscheinlich der Fall zu sein. Bitte korrigieren, wenn das nicht der Fall ist.

Ich habe also die folgenden 2 Klassen, die über Vererbung verwandt sind.

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

Als Nächstes füge ich wie folgt eine Methode $#$ mapObject $#$ in der Klasse SubTarget hinzu.

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

Obwohl ich erwartet hatte, dass dies funktioniert, erhalte ich die folgende Fehlermeldung.

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'.

Wird dieser Fehler aufgrund dieses Problems generiert? Ansonsten wäre eine Erklärung wirklich toll.

Link zum Spielplatz

Kam hierher, als ich nach einer Lösung suchte, um statische Methoden zu kommentieren, die neue Instanzen der eigentlichen Klasse zurückgeben, die aufgerufen werden.

Ich denke, dass die von @SMotaal vorgeschlagene Problemumgehung (in Kombination mit new this() ) die sauberste ist und für mich am sinnvollsten ist. Es erlaubt, die gewünschte Absicht auszudrücken, zwingt mich nicht, den Typ bei jedem Aufruf einer generischen Methode anzugeben, und fügt dem endgültigen Code keinen Overhead hinzu.

Aber im Ernst, warum ist das noch nicht Teil des Core Typescript? Es ist ein ziemlich häufiges Szenario.

@danielvy – Ich denke, dass die Trennung zwischen OOP und Prototypen die größere mentale Kluft ist, die nicht alle glücklich macht. Ich denke, dass die Kernannahmen über Klassen, die lange vor der Entwicklung der Klassenfeatures in der Spezifikation getroffen wurden, nicht übereinstimmten, und davon abzuweichen, ist eine Falle für viele funktionierende TypeScript-Features, also „priorisiert“ jeder, und das ist in Ordnung, aber nicht für „ Typen" aus meiner Sicht. Keine Lösung zu haben, die besser ist als die der Konkurrenz, ist nur dann ein Problem, wenn es Konkurrenz gibt, deshalb sind alle Eier in einen Korb immer schlecht für alle – ich bin hier pragmatisch und ehrlich.

Das funktioniert nicht:

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.

Aber sollte dies nicht erlaubt sein, da Extension Base erweitert?

Beachten Sie, dass dies funktioniert:

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

aber dann können Sie this.member nicht darin verwenden.

Ich vermute also aus dem @ntucker- Post, dass die Problemumgehung nur eine Ebene tief funktioniert, es sei denn, die erweiterte Klasse stimmt genau mit der Basisklasse überein?

Hallo! Was ist mit geschützten Konstruktoren?

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

  protected constructor() {}
}

class B extends A {} 

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

Wenn der Konstruktor öffentlich ist, ist es in Ordnung, aber wenn nicht, verursacht es einen Fehler:

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.

Spielplatz - http://tiny.cc/r74c9y

Gibt es Hoffnung dafür? Bin gerade wieder über dieses Bedürfnis gestolpert :F

@ntucker Duuuuudee, du hast es geschafft!!! Die wichtigste Erkenntnis für mich war, die statische create-Methode generisch zu machen, this param zu deklarieren und dann am Ende die InstanceType<U> #$-Umwandlung durchzuführen.

Spielplatz

Von oben ^

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 😄 

Dies erfüllt meinen Anwendungsfall perfekt und scheint ab TS 3.5.1 auch die Mehrheit der Leute in diesem Thread zu befriedigen (siehe Spielplatz für die Erforschung der statischen Requisite theAnswer sowie der 3. Ebene der Erweiterung).

Hot Take: Ich denke, das funktioniert jetzt und kann geschlossen werden?

@jonjaques Übrigens verwendest du nie T. Außerdem löst dies nicht das Problem, das ich skizziert habe und das Methoden überschreibt.

Ich habe das Gefühl, dass die erwähnte Lösung bereits vor fast 4 Jahren vorgeschlagen wurde… Und doch ist es nicht etwas, das das Problem löst.

Ich habe einen Stapelüberlauf geöffnet, um zu sehen, ob es andere Problemumgehungen gibt, die jemand kennen könnte, und jemand hatte eine gute Zusammenfassung des Problems, das ich erlebe:

"Generika, die als Methodenparameter verwendet werden, einschließlich dieser Parameter, scheinen kontravariant zu sein, aber eigentlich möchten Sie, dass diese Parameter immer als kovariant (oder vielleicht sogar bivariant) behandelt werden."

Es scheint also, als wäre dies wirklich etwas, das in TypeScript selbst defekt ist und behoben werden sollte ("this" sollte kovariant oder bivariant sein).

EDIT: Dieser Kommentar hat einen besseren Weg.

Ich habe nicht viel hinzuzufügen, aber das hat für mich von https://github.com/microsoft/TypeScript/issues/5863#issuecomment -437217433 gut funktioniert

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()

Ich hoffe, dies ist für alle nützlich, die diese lange Ausgabe nicht durchblättern möchten.

Ein weiterer Anwendungsfall sind benutzerdefinierte Typwächter-Memberfunktionen für Nicht-Klassen:

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

Die Problemumgehung bringt mich fast dorthin, wo ich sein muss, aber ich muss noch einen Schraubenschlüssel in die Gänge werfen. Nehmen Sie das folgende erfundene Beispiel:

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

Hier ist der Fehler:

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)

Das macht für mich Sinn, da die Methode create in der Klasse Automobile keinen Verweis auf ITruckOptions bis O hat, aber ich frage mich, ob es einen gibt Problemumgehung für dieses Problem?

Normalerweise erweitern die Optionen meiner Erweiterungsklasse für den Konstruktor die Optionsschnittstelle der Basisklasse, daher weiß ich, dass sie immer die Parameter für die Basisklasse enthalten, aber ich habe keine zuverlässige Möglichkeit, sicherzustellen, dass sie die Parameter für die Erweiterung enthalten Klasse.

Ich musste auch darauf zurückgreifen, Methoden beim Erweitern von Klassen zu überschreiben, um sie über die erwarteten Eingabe- und Rückgabetypen der erbenden Klasse zu informieren, was sich nur ein bisschen unangenehm anfühlt.

@Jack-Barry das funktioniert:

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

Es ist jedoch immer noch nicht ideal, da Konstruktoren öffentlich sind, sodass es möglich ist, einfach new Automobile({ make: "Audi" }) .

@elderapo Ich denke, das bringt mir, was ich brauche - das Beispiel ist natürlich erfunden, aber ich sehe, wo mich mein mangelndes Verständnis von _Generika_ ein wenig gebissen hat. Danke fürs Aufräumen!

Hoffentlich hilft dies anderen, lassen Sie mich wissen, ob es Raum für Verbesserungen gibt. Es ist nicht perfekt, da die Lesefunktionen möglicherweise nicht mehr mit den Eingaben synchron sind, aber es ist das nächste, was ich dem benötigten Muster erreichen konnte.

// 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)

Sie könnten dieses Muster problemlos auf die verbleibenden CRUD-Aktionen erweitern.

Im Beispiel von @Jack-Barry versuche ich, die Funktion read dazu zu bringen, eine Instanz der Klasse zurückzugeben. Ich habe erwartet, dass Folgendes funktioniert:

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

aber bekomme stattdessen den Fehler Property 'create' does not exist on type 'ThisClass<A, T>'. Hätte jemand eine Problemumgehung, wie man das angeht?

@everhardt Da die Methode create static ist, müsste sie für class von this aufgerufen werden, nicht für die Instanz. Das heißt, ich habe nicht wirklich eine gute Problemumgehung für den dynamischen Zugriff auf die class -Funktion, die immer noch die gewünschte Typüberprüfung bietet.

Das Beste, was ich erreichen kann, ist nicht besonders elegant und kann die vorhandenen type von BaseClass.create nicht verwenden, sodass sie leicht nicht mehr synchron sein könnten:

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

Ich habe das _ nicht _ getestet.

@Jack-Barry dann bekommst du this.constructor.create is not a function .

Das macht Sinn: Typescript interpretiert this als Instanz, aber für den eventuellen Javascript-Parser ist this die Klasse, da die Funktion statisch ist.

Vielleicht nicht die genialste Erweiterung von Jack-Barrys Beispiel, aber unter diesem Playground Link können Sie sehen, wie ich es gelöst habe.

Das Kreuz:

  • Der Typ ThisClass sollte alle statischen Eigenschaften und Methoden beschreiben, die Methoden von BaseClass (sowohl statisch als auch auf der Instanz) mit einem polymorphen 'this' verwenden möchten.
  • Wenn Sie eine statische Eigenschaft oder Methode in einer Instanzmethode verwenden, verwenden (this.constructor as ThisClass<A, this>)

Es ist doppelte Arbeit, da Sie die Typen statischer Methoden und Eigenschaften sowohl für die Klasse als auch für den Typ ThisClass definieren müssen, aber für mich funktioniert es.

Edit: Playground-Link korrigiert

PHP hat dieses Problem vor langer Zeit behoben. selbst. statisch. Dies.

Hallo zusammen, es ist 2020 und wir haben _dieses_ Problem. 😛

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

Ich würde erwarten, dass bugs eine Instanz von Bunny ist.

Was ist die aktuelle Problemumgehung? Ich habe alles in diesem Problem versucht, nichts scheint zu beheben.

Update : Das hat funktioniert, die Definition des this -Arguments fehlte.

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

Das Problem dabei ist, dass Getter und Setter keine Typparameter haben können.

Es ist auch zu ausführlich.

Ich beschäftige mich mit einem ähnlichen Problem. Um eine "polymorphe innere Klasse" zu implementieren, konnte ich Folgendes am besten finden:

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

Es scheint gut zu funktionieren, aber die Eingabe von createInnerClass() ist meiner Meinung nach viel zu ausführlich, und ich werde viele davon in meiner Codebasis haben. Hat jemand eine Idee, wie man diesen Code vereinfacht?

Das Problem bei diesem Ansatz ist, dass er mit statischen Gettern und Settern nicht funktioniert.

Wie wäre es mit der Implementierung self. für statische Eigenschaften? Ich hatte dieses Problem vor 4 Jahren und komme mit demselben Problem auf denselben Thread zurück.

@sudomaxime Das liegt außerhalb des Rahmens dieses Problems und widerspricht den Designzielen von TypeScript , solange ECMAScript self. nicht nativ als Alias ​​für this.constructor. in Klassen unterstützt, was wahrscheinlich nicht der Fall ist passieren, da self ein allgemeiner Variablenname ist.

Die Lösung von @abdullah-kasim scheint wie beschrieben zu funktionieren, aber ich kann sie nicht mit generischen Klassen zum Laufen bringen:

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 =  )
   |   `  |
  /|       |\
  \| |   | |/
 (_|_|___|_|_)
   '"'   '"'

Ich würde mich über Gedanken freuen, ob dies machbar ist, da ich ein bisschen ohne Glück daran herumgebastelt habe.

@stevehanson Würde das für dich funktionieren?

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

Getestet auf Typescript Playground.

Inspiriert von diesem Link:
https://selleo.com/til/posts/gll9bsvjcj-generic-with-class-as-argument-and-returning-instance

Und ich bin mir nicht sicher, wie es geschafft hat zu implizieren, dass T die Klasse selbst ist . EDIT: Ah, ich erinnerte mich, es gelang, T zu implizieren, weil der stille Parameter this immer die Klasse selbst passieren wird.

@abdullah-kasim das hat funktioniert! Wow Danke! Für meinen speziellen Fall nimmt mein Konstruktor ein Argument, also sah es für mich so aus, falls es jemand anderem hilft:

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

Einige der Lösungen hier funktionieren nicht für statische Getter, da ich keine Argumente übergeben kann:

Wie kann ich im folgenden Beispiel den statischen Getter default in eine übergeordnete Klasse verschieben?

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

const x = Letters.default.alpha;

Dies kann eine Überlegung wert sein, wenn wir jemals Syntaxverbesserungen in Betracht ziehen.

Möglicherweise verwandt: Ich habe Schwierigkeiten, dem Beispiel von @abdullah-kasim eine weitere Abstraktionsebene hinzuzufügen. Im Wesentlichen möchte ich in der Lage sein, Animal in eine Schnittstelle zu verwandeln und Bunny zu erlauben, seine eigene statische create() $ zu definieren.

Hier ist ein praktisches Beispiel (verzeihen Sie mir, dass ich die Bunny-Analogie aufgegeben habe!). Ich möchte in der Lage sein, ein paar verschiedene Dokumenttypen zu definieren, und jeder sollte in der Lage sein, eine statische Factory-Methode zu definieren, die eine Zeichenfolge in eine Dokumentinstanz umwandelt. Ich möchte erzwingen, dass ein Dokumenttyp nur einen Parser für sich selbst und nicht für andere Dokumenttypen definieren muss.

(Die Typen im Beispiel unten sind nicht ganz richtig - hoffentlich ist klar genug, was ich versuche zu erreichen)

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

(glaube ich) ich würde gerne so etwas schreiben können:

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

Hast du eine Idee, ob auch hier ein Workaround möglich ist?

EDIT: Mögliche Problemumgehung

Ein weiterer Anwendungsfall für StackOverflow
Zum Entwerfen einer generischen Nachschlagetabellenklasse.

// 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 kommentierte am 29. Mai 2016
😰 Kann nicht glauben, dass das immer noch nicht behoben / hinzugefügt wurde.....

ha ha
2020 ist da!

Das ist lächerlich und verrückt. Es ist 2020. 4 Jahre später, warum wurde das nicht behoben?

Warum nicht so etwas implementieren

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
War diese Seite hilfreich?
0 / 5 - 0 Bewertungen

Verwandte Themen

siddjain picture siddjain  ·  3Kommentare

seanzer picture seanzer  ·  3Kommentare

MartynasZilinskas picture MartynasZilinskas  ·  3Kommentare

manekinekko picture manekinekko  ·  3Kommentare

Antony-Jones picture Antony-Jones  ·  3Kommentare