Erstellen Sie ein neues Objekt aus dem Typparameter in der generischen Klasse

144

Ich versuche, ein neues Objekt eines Typparameters in meiner generischen Klasse zu erstellen. In meiner Klasse habe Viewich zwei Listen von Objekten vom generischen Typ, die als Typparameter übergeben wurden. Wenn ich jedoch versuche, sie zu new TGridView()erstellen, sagt TypeScript:

Symbol 'TGridView konnte nicht gefunden werden

Dies ist der Code:

module AppFW {
    // Represents a view
    export class View<TFormView extends FormView, TGridView extends GridView> {
        // The list of forms 
        public Forms: { [idForm: string]: TFormView; } = {};

        // The list of grids
        public Grids: { [idForm: string]: TGridView; } = {};

        public AddForm(formElement: HTMLFormElement, dataModel: any, submitFunction?: (e: SubmitFormViewEvent) => boolean): FormView {
            var newForm: TFormView = new TFormView(formElement, dataModel, submitFunction);
            this.Forms[formElement.id] = newForm;
            return newForm;
        }

        public AddGrid(element: HTMLDivElement, gridOptions: any): GridView {
            var newGrid: TGridView = new TGridView(element, gridOptions);
            this.Grids[element.id] = newGrid;
            return newGrid;
        }
    }
}

Kann ich Objekte aus einem generischen Typ erstellen?

Javier Ros
quelle

Antworten:

95

Da im kompilierten JavaScript alle Typinformationen gelöscht wurden, können Sie sie nicht verwenden T Objekt neu erstellen.

Sie können dies auf nicht generische Weise tun, indem Sie den Typ an den Konstruktor übergeben.

class TestOne {
    hi() {
        alert('Hi');
    }
}

class TestTwo {
    constructor(private testType) {

    }
    getNew() {
        return new this.testType();
    }
}

var test = new TestTwo(TestOne);

var example = test.getNew();
example.hi();

Sie können dieses Beispiel mithilfe von Generika erweitern, um die Typen zu straffen:

class TestBase {
    hi() {
        alert('Hi from base');
    }
}

class TestSub extends TestBase {
    hi() {
        alert('Hi from sub');
    }
}

class TestTwo<T extends TestBase> {
    constructor(private testType: new () => T) {
    }

    getNew() : T {
        return new this.testType();
    }
}

//var test = new TestTwo<TestBase>(TestBase);
var test = new TestTwo<TestSub>(TestSub);

var example = test.getNew();
example.hi();
Fenton
quelle
144

Um ein neues Objekt in generischem Code zu erstellen, müssen Sie über seine Konstruktorfunktion auf den Typ verweisen. Also anstatt dies zu schreiben:

function activatorNotWorking<T extends IActivatable>(type: T): T {
    return new T(); // compile error could not find symbol T
}

Sie müssen dies schreiben:

function activator<T extends IActivatable>(type: { new(): T ;} ): T {
    return new type();
}

var classA: ClassA = activator(ClassA);

Siehe diese Frage: Generische Typinferenz mit Klassenargument

Blorkfish
quelle
35
Etwas später (aber nützlicher) Kommentar - wenn Ihr Konstruktor Argumente nimmt, dann new()muss das auch - oder ein allgemeinerer:type: {new(...args : any[]): T ;}
Rycochet
1
@ Yoda IActivatable ist eine selbst erstellte Schnittstelle, siehe andere Frage: stackoverflow.com/questions/24677592/…
Jamie
1
Wie sollen diese geschlossen werden { new(): T ;}. Es fällt mir schwer, diesen Teil zu lernen.
Abitcode
1
Dies (und alle anderen Lösungen hier) erfordern, dass Sie die Klasse zusätzlich zur Angabe des Generikums übergeben. Das ist hackig und überflüssig. Es bedeutet, wenn ich eine Basisklasse habe ClassA<T>und sie erweitere ClassB extends ClassA<MyClass>, muss ich auch übergeben new () => T = MyClassoder keine dieser Lösungen funktioniert.
AndrewBenjamin
@ AndrewBenjamin Was ist dein Vorschlag dann? Wenn Sie nicht versuchen, etwas zu starten (eigentlich wollen Sie es lernen), sondern es als hackig und überflüssig bezeichnen, wissen Sie, dass Sie einen anderen Weg kennen, den Sie nicht teilen :)
Perry,
37

Alle Typinformationen werden auf der JavaScript-Seite gelöscht, und daher können Sie T nicht wie bei @Sohnee neu angeben, aber ich würde es vorziehen, wenn typisierte Parameter an den Konstruktor übergeben werden:

class A {
}

class B<T> {
    Prop: T;
    constructor(TCreator: { new (): T; }) {
        this.Prop = new TCreator();
    }
}

var test = new B<A>(A);
TadasPa
quelle
12
export abstract class formBase<T> extends baseClass {

  protected item = {} as T;
}

Sein Objekt kann jeden Parameter empfangen, Typ T ist jedoch nur eine Typoskriptreferenz und kann nicht über einen Konstruktor erstellt werden. Das heißt, es werden keine Klassenobjekte erstellt.

jales cardoso
quelle
6
Beachten Sie , dass dies möglicherweise funktioniert, das resultierende Objekt jedoch nicht vom Typ ist T, sodass alle in definierten Getter / Setter Tmöglicherweise nicht funktionieren.
Daniel Ormeño
@ DanielOrmeño willst du das erklären? Was meinst du damit? Mein Code scheint mit diesem Snippet zu funktionieren. Vielen Dank.
Eestein
Javascript ist eine interpretierte Sprache und Ecmascript enthält noch keine Verweise auf Typen oder Klassen. T ist also nur eine Referenz für Typoskript.
Jales Cardoso
Dies ist eine falsche Antwort, auch in JavaScript. Wenn Sie eine haben class Foo{ method() { return true; }, erhalten Sie diese Methode nicht, wenn Sie {} auf T setzen!
JCKödel
8

Ich weiß es spät, aber die Antwort von @ TadasPa kann mit etwas angepasst werden

TCreator: new() => T

anstatt

TCreator: { new (): T; }

Das Ergebnis sollte also so aussehen

class A {
}

class B<T> {
    Prop: T;
    constructor(TCreator: new() => T) {
        this.Prop = new TCreator();
    }
}

var test = new B<A>(A);
Jazib
quelle
4

Ich habe versucht, das Generikum aus einer Basisklasse heraus zu instanziieren. Keines der oben genannten Beispiele hat für mich funktioniert, da ein konkreter Typ erforderlich war, um die Factory-Methode aufzurufen.

Nachdem ich eine Weile darüber recherchiert hatte und online keine Lösung gefunden hatte, stellte ich fest, dass dies anscheinend funktioniert.

 protected activeRow: T = {} as T;

Die Stücke:

 activeRow: T = {} <-- activeRow now equals a new object...

...

 as T; <-- As the type I specified. 

Alle zusammen

 export abstract class GridRowEditDialogBase<T extends DataRow> extends DialogBase{ 
      protected activeRow: T = {} as T;
 }

Das heißt, wenn Sie eine tatsächliche Instanz benötigen, sollten Sie Folgendes verwenden:

export function getInstance<T extends Object>(type: (new (...args: any[]) => T), ...args: any[]): T {
      return new type(...args);
}


export class Foo {
  bar() {
    console.log("Hello World")
  }
}
getInstance(Foo).bar();

Wenn Sie Argumente haben, können Sie verwenden.

export class Foo2 {
  constructor(public arg1: string, public arg2: number) {

  }

  bar() {
    console.log(this.arg1);
    console.log(this.arg2);
  }
}
getInstance(Foo, "Hello World", 2).bar();
Christian Patzer
quelle
2
Dies ist eine falsche Antwort, auch in JavaScript. Wenn Sie eine Klasse haben Foo {method () {return true; } Wenn Sie {} auf T setzen, erhalten Sie diese Methode nicht!
JCKödel
Ich arbeite, wenn Sie keine Methoden haben. Wenn Sie jedoch ein Objekt mit Methoden haben, müssen Sie den Code verwenden, den ich gerade hinzugefügt habe.
Christian Patzer
2

Dies ist, was ich tue, um Typinformationen beizubehalten:

class Helper {
   public static createRaw<T>(TCreator: { new (): T; }, data: any): T
   {
     return Object.assign(new TCreator(), data);
   }
   public static create<T>(TCreator: { new (): T; }, data: T): T
   {
      return this.createRaw(TCreator, data);
   }
}

...

it('create helper', () => {
    class A {
        public data: string;
    }
    class B {
        public data: string;
        public getData(): string {
            return this.data;
        }
    }
    var str = "foobar";

    var a1 = Helper.create<A>(A, {data: str});
    expect(a1 instanceof A).toBeTruthy();
    expect(a1.data).toBe(str);

    var a2 = Helper.create(A, {data: str});
    expect(a2 instanceof A).toBeTruthy();
    expect(a2.data).toBe(str);

    var b1 = Helper.createRaw(B, {data: str});
    expect(b1 instanceof B).toBeTruthy();
    expect(b1.data).toBe(str);
    expect(b1.getData()).toBe(str);

});
Saturnus
quelle
1

Ich benutze dies: let instance = <T>{}; es funktioniert im Allgemeinen EDIT 1:

export class EntityCollection<T extends { id: number }>{
  mutable: EditableEntity<T>[] = [];
  immutable: T[] = [];
  edit(index: number) {
    this.mutable[index].entity = Object.assign(<T>{}, this.immutable[index]);
  }
}
Bojo
quelle
5
Dadurch wird keine neue Instanz von instanziiert T, sondern es wird lediglich ein leeres Objekt erstellt, von dem TypeScript glaubt , dass es vom Typ ist T. Vielleicht möchten Sie Ihre Antwort etwas ausführlicher erläutern.
Sandy Gifford
Ich sagte, dass es im Allgemeinen funktioniert. Es ist genau so, wie du sagst. Der Compiler jammert nicht und Sie haben generischen Code. Mit dieser Instanz können Sie die gewünschten Eigenschaften manuell festlegen, ohne dass ein expliziter Konstruktor erforderlich ist.
Bojo
1

Die Frage wird nicht ganz beantwortet, aber es gibt eine nette Bibliothek für diese Art von Problemen: https://github.com/typestack/class-transformer (obwohl es für generische Typen nicht funktioniert, da sie nicht wirklich existieren zur Laufzeit (hier wird die ganze Arbeit mit Klassennamen (die Klassenkonstruktoren sind) erledigt))

Zum Beispiel:

import {Type, plainToClass, deserialize} from "class-transformer";

export class Foo
{
    @Type(Bar)
    public nestedClass: Bar;

    public someVar: string;

    public someMethod(): string
    {
        return this.nestedClass.someVar + this.someVar;
    }
}

export class Bar
{
    public someVar: string;
}

const json = '{"someVar": "a", "nestedClass": {"someVar": "B"}}';
const optionA = plainToClass(Foo, JSON.parse(json));
const optionB = deserialize(Foo, json);

optionA.someMethod(); // works
optionB.someMethod(); // works
JCKödel
quelle
1

Ich bin zu spät zur Party, aber so habe ich es zum Laufen gebracht. Für Arrays müssen wir einige Tricks machen:

   public clone<T>(sourceObj: T): T {
      var cloneObj: T = {} as T;
      for (var key in sourceObj) {
         if (sourceObj[key] instanceof Array) {
            if (sourceObj[key]) {
               // create an empty value first
               let str: string = '{"' + key + '" : ""}';
               Object.assign(cloneObj, JSON.parse(str))
               // update with the real value
               cloneObj[key] = sourceObj[key];
            } else {
               Object.assign(cloneObj, [])
            }
         } else if (typeof sourceObj[key] === "object") {
            cloneObj[key] = this.clone(sourceObj[key]);
         } else {
            if (cloneObj.hasOwnProperty(key)) {
               cloneObj[key] = sourceObj[key];
            } else { // insert the property
               // need create a JSON to use the 'key' as its value
               let str: string = '{"' + key + '" : "' + sourceObj[key] + '"}';
               // insert the new field
               Object.assign(cloneObj, JSON.parse(str))
            }
         }
      }
      return cloneObj;
   }

Verwenden Sie es so:

  let newObj: SomeClass = clone<SomeClass>(someClassObj);

Es kann verbessert werden, aber für meine Bedürfnisse gearbeitet!

EQuadrado
quelle
1

Ich füge dies auf Anfrage hinzu, nicht weil ich denke, dass es die Frage direkt löst. Meine Lösung umfasst eine Tabellenkomponente zum Anzeigen von Tabellen aus meiner SQL-Datenbank:

export class TableComponent<T> {

    public Data: T[] = [];

    public constructor(
        protected type: new (value: Partial<T>) => T
    ) { }

    protected insertRow(value: Partial<T>): void {
        let row: T = new this.type(value);
        this.Data.push(row);
    }
}

Angenommen, ich habe eine Ansicht (oder Tabelle) in meiner Datenbank VW_MyData und möchte den Konstruktor meiner VW_MyData-Klasse für jeden von einer Abfrage zurückgegebenen Eintrag aufrufen:

export class MyDataComponent extends TableComponent<VW_MyData> {

    public constructor(protected service: DataService) {
        super(VW_MyData);
        this.query();
    }

    protected query(): void {
        this.service.post(...).subscribe((json: VW_MyData[]) => {
            for (let item of json) {
                this.insertRow(item);
            }
        }
    }
}

Der Grund, warum dies wünschenswert ist, wenn der zurückgegebene Wert einfach Data zugewiesen wird, ist, dass ich einen Code habe, der eine Transformation auf eine Spalte von VW_MyData in seinem Konstruktor anwendet:

export class VW_MyData {
    
    public RawColumn: string;
    public TransformedColumn: string;


    public constructor(init?: Partial<VW_MyData>) {
        Object.assign(this, init);
        this.TransformedColumn = this.transform(this.RawColumn);
    }

    protected transform(input: string): string {
        return `Transformation of ${input}!`;
    }
}

Auf diese Weise kann ich Transformationen, Validierungen und alles andere für alle meine in TypeScript eingehenden Daten durchführen. Hoffentlich bietet es jemandem einen Einblick.

AndrewBenjamin
quelle