Utwórz nowy obiekt z parametru typu w klasie ogólnej

145

Próbuję utworzyć nowy obiekt parametru typu w mojej klasie ogólnej. W mojej klasie Viewmam 2 listy obiektów typu ogólnego przekazanych jako parametry typu, ale kiedy próbuję zrobić new TGridView(), TypeScript mówi:

Nie można znaleźć symbolu „TGridView

To jest kod:

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

Czy mogę tworzyć obiekty z typu ogólnego?

Javier Ros
źródło

Odpowiedzi:

96

Ponieważ skompilowany JavaScript ma usunięte wszystkie informacje o typie, nie możesz użyć go Tdo utworzenia nowego obiektu.

Możesz to zrobić w sposób inny niż ogólny, przekazując typ do konstruktora.

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

Możesz rozszerzyć ten przykład, używając typów ogólnych, aby zaostrzyć typy:

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
źródło
144

Aby utworzyć nowy obiekt w kodzie ogólnym, musisz odwołać się do typu za pomocą funkcji konstruktora. Więc zamiast pisać to:

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

Musisz to napisać:

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

var classA: ClassA = activator(ClassA);

Zobacz to pytanie: Wnioskowanie o typie ogólnym z argumentem klasy

blorkfish
źródło
35
Nieco późny (ale przydatny) komentarz - jeśli twój konstruktor przyjmuje argumenty, to new()również jest to konieczne - lub bardziej ogólny:type: {new(...args : any[]): T ;}
Rycochet
1
@Yoda IActivatable to samodzielnie stworzony interfejs, zobacz inne pytanie: stackoverflow.com/questions/24677592/…
Jamie
1
Jak należy to wywnioskować { new(): T ;}. Ciężko mi jest nauczyć się tej części.
kod abitcode
1
To (i wszystkie inne rozwiązania tutaj) wymagają zaliczenia klasy oprócz określenia standardu. To jest hacky i zbędne. Oznacza to, że jeśli mam klasę bazową ClassA<T>i rozszerzam ją ClassB extends ClassA<MyClass>, to też muszę przejść new () => T = MyClasslub żadne z tych rozwiązań nie działa.
AndrewBenjamin
@AndrewBenjamin Jaka jest więc twoja sugestia? Nie próbuję niczego zaczynać (właściwie chcę się nauczyć), ale nazywanie tego hackem i zbędnymi wnioskami oznacza, że ​​znasz inny sposób, którego nie udostępniasz :)
perry
37

Wszystkie informacje o typie są usuwane po stronie JavaScript i dlatego nie można tworzyć nowych T, tak jak stany @Sohnee, ale wolałbym, aby wpisany parametr został przekazany do konstruktora:

class A {
}

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

var test = new B<A>(A);
TadasPa
źródło
12
export abstract class formBase<T> extends baseClass {

  protected item = {} as T;
}

Jego obiekt będzie mógł otrzymać dowolny parametr, jednak typ T jest tylko odwołaniem do maszynopisu i nie można go utworzyć za pomocą konstruktora. Oznacza to, że nie utworzy żadnych obiektów klas.

jales cardoso
źródło
6
Uważaj , to może działać, ale wynikowy obiekt nie będzie typu Type T, więc wszelkie metody pobierające / ustawiające zdefiniowane w Tmogą nie działać.
Daniel Ormeño,
@ DanielOrmeño chcesz wyjaśnić? Co przez to rozumiesz? Wygląda na to, że mój kod działa przy użyciu tego fragmentu. Dzięki.
eestein
Javascript jest językiem interpretowanym, a ekmascript nie zawiera jeszcze odniesień do typów ani klas. Więc T jest tylko odniesieniem do maszynopisu.
jales cardoso
To ZŁA odpowiedź, nawet w JavaScript. Jeśli masz class Foo{ method() { return true; }, rzutowanie {} na T nie da ci tej metody!
JCKödel
9

Wiem późno, ale odpowiedź @ TadasPa można trochę skorygować za pomocą

TCreator: new() => T

zamiast

TCreator: { new (): T; }

więc wynik powinien wyglądać tak

class A {
}

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

var test = new B<A>(A);
Jazib
źródło
4

Próbowałem utworzyć instancję generyczną z poziomu klasy bazowej. Żaden z powyższych przykładów nie zadziałał dla mnie, ponieważ wymagały konkretnego typu, aby wywołać metodę fabryczną.

Po dłuższych poszukiwaniach na ten temat i nie mogąc znaleźć rozwiązania online, odkryłem, że wydaje się to działać.

 protected activeRow: T = {} as T;

Kawałki:

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

...

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

Wszyscy razem

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

To powiedziawszy, jeśli potrzebujesz rzeczywistej instancji, powinieneś użyć:

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

Jeśli masz argumenty, możesz użyć.

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
źródło
2
To ZŁA odpowiedź, nawet w JavaScript. Jeśli masz klasę Foo {method () {return true; }, rzutowanie {} na T nie da ci tej metody!
JCKödel
Pracuję, jeśli nie masz metod. Jeśli jednak masz obiekt z metodami, musisz użyć kodu, który właśnie dodałem.
Christian Patzer
2

Oto, co robię, aby zachować informacje o typie:

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
źródło
1

używam tego: let instance = <T>{}; generalnie działa EDYCJA 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
źródło
5
To nie będzie faktycznie instancję nową instancję T, będzie to po prostu zrobić pustego obiektu, że maszynopis myśli jest typu T. Możesz bardziej szczegółowo wyjaśnić swoją odpowiedź.
Sandy Gifford
Powiedziałem, że ogólnie działa. Jest dokładnie tak, jak mówisz. Kompilator nie marudzi i masz ogólny kod. Możesz użyć tego wystąpienia, aby ręcznie ustawić żądane właściwości bez potrzeby jawnego konstruktora.
Bojo
1

Nie do końca odpowiadając na pytanie, ale jest fajna biblioteka dla tego rodzaju problemów: https://github.com/typestack/class-transformer (chociaż nie będzie działać dla typów ogólnych, ponieważ tak naprawdę nie istnieją w czasie wykonywania (tutaj cała praca jest wykonywana z nazwami klas (które są konstruktorami klas)))

Na przykład:

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
źródło
1

Spóźniłem się na imprezę, ale tak to działało. W przypadku tablic musimy zrobić kilka sztuczek:

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

Użyj tego w ten sposób:

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

Można to poprawić, ale działało na moje potrzeby!

EQuadrado
źródło
1

Dodam to na żądanie, a nie dlatego, że myślę, że to bezpośrednio rozwiązuje kwestię. Moje rozwiązanie obejmuje komponent tabeli do wyświetlania tabel z mojej bazy danych SQL:

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

Aby to wykorzystać, załóżmy, że mam widok (lub tabelę) w mojej bazie danych VW_MyData i chcę trafić do konstruktora mojej klasy VW_MyData dla każdego wpisu zwróconego z zapytania:

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

Powodem, dla którego jest to pożądane w porównaniu z prostym przypisywaniem zwracanej wartości do danych, jest to, że mam kod, który stosuje transformację do jakiejś kolumny VW_MyData w swoim konstruktorze:

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

To pozwala mi wykonywać transformacje, walidacje i cokolwiek innego na wszystkich moich danych przychodzących do TypeScript. Miejmy nadzieję, że zapewni to komuś wgląd.

AndrewBenjamin
źródło