Crea un nuovo oggetto dal parametro type nella classe generica


144

Sto cercando di creare un nuovo oggetto di un parametro di tipo nella mia classe generica. Nella mia classe View, ho 2 elenchi di oggetti di tipo generico passati come parametri di tipo, ma quando provo a creare new TGridView(), TypeScript dice:

Impossibile trovare il simbolo 'TGridView

Questo è il codice:

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

Posso creare oggetti da un tipo generico?

Risposte:


95

Poiché nel JavaScript compilato sono state cancellate tutte le informazioni sul tipo, non è possibile utilizzare Tper rinnovare un oggetto.

Puoi farlo in modo non generico passando il tipo nel costruttore.

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

È possibile estendere questo esempio usando generici per rafforzare i tipi:

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

144

Per creare un nuovo oggetto all'interno di un codice generico, è necessario fare riferimento al tipo tramite la sua funzione di costruzione. Quindi invece di scrivere questo:

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

Devi scrivere questo:

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

var classA: ClassA = activator(ClassA);

Vedi questa domanda: Inferenza di tipo generico con argomento di classe


35
Commento un po 'in ritardo (ma utile) - se il tuo costruttore accetta args anche le new()necessità - o un più generico:type: {new(...args : any[]): T ;}
Rycochet

1
@Yoda IActivatable è un'interfaccia creata da te, vedi altra domanda: stackoverflow.com/questions/24677592/…
Jamie

1
Come dovrebbe essere dedotto questo { new(): T ;}. Non riesco a imparare questa parte.
abitcode

1
Questo (e tutte le altre soluzioni qui) richiedono di passare alla classe oltre a specificare il generico. Questo è confuso e ridondante. Significa che se ho una classe base ClassA<T>e la estendo ClassB extends ClassA<MyClass>, devo anche passare new () => T = MyClasso nessuna di queste soluzioni funziona.
AndrewBenjamin,

@AndrewBenjamin Qual è il tuo suggerimento allora? Non provando ad avviare nulla (in realtà vuoi imparare) ma chiamandolo hacky e ridondanti ti informi di un altro modo che non stai condividendo :)
perry

37

Tutte le informazioni sul tipo vengono cancellate nel lato JavaScript e quindi non è possibile rinnovare T proprio come gli stati @Sohnee, ma preferirei che il parametro digitato fosse passato al costruttore:

class A {
}

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

var test = new B<A>(A);

12
export abstract class formBase<T> extends baseClass {

  protected item = {} as T;
}

Il suo oggetto sarà in grado di ricevere qualsiasi parametro, tuttavia, il tipo T è solo un riferimento dattiloscritto e non può essere creato tramite un costruttore. Cioè, non creerà alcun oggetto di classe.


6
Attenzione , questo potrebbe funzionare ma l'oggetto risultante non sarà di Tipo T, quindi eventuali getter / setter definiti in Tpotrebbero non funzionare.
Daniel Ormeño,

@ DanielOrmeño cura di spiegare? Cosa vuoi dire con questo? Il mio codice sembra funzionare usando questo frammento. Grazie.
est

Javascript è un linguaggio interpretato ed ecmascript non ha ancora riferimenti a tipi o classi. Quindi T è solo un riferimento per dattiloscritto.
jales cardoso,

Questa è una risposta ERRATA, anche in JavaScript. Se hai un class Foo{ method() { return true; }, lanciare {} su T non ti darà questo metodo!
JCKödel,

8

Lo so tardi ma la risposta di @ TadasPa può essere modificata un po 'usando

TCreator: new() => T

invece di

TCreator: { new (): T; }

quindi il risultato dovrebbe apparire così

class A {
}

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

var test = new B<A>(A);

4

Stavo cercando di creare un'istanza del generico all'interno di una classe base. Nessuno degli esempi sopra ha funzionato per me in quanto richiedevano un tipo concreto per chiamare il metodo factory.

Dopo aver cercato per un po 'su questo e incapace di trovare una soluzione online, ho scoperto che questo sembra funzionare.

 protected activeRow: T = {} as T;

I pezzi:

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

...

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

Tutti insieme

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

Detto questo, se hai bisogno di un'istanza reale dovresti usare:

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

Se hai argomenti, puoi usare.

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

2
Questa è una risposta ERRATA, anche in JavaScript. Se hai una classe Foo {method () {return true; }, lanciare {} su T non ti darà questo metodo!
JCKödel,

Io lavoro se non hai metodi. Se tuttavia hai un oggetto con metodi devi usare il codice che ho appena aggiunto.
Christian Patzer,

2

Questo è ciò che faccio per conservare le informazioni sul tipo:

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

});

1

lo uso: let instance = <T>{}; generalmente funziona 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]);
  }
}

5
Questo in realtà non crea un'istanza di una nuova istanza T, ma crea semplicemente un oggetto vuoto che TypeScript ritiene sia di tipo T. Potresti voler spiegare la tua risposta in modo un po 'più dettagliato.
Sandy Gifford,

Ho detto che generalmente funziona. È esattamente come dici tu. Il compilatore non è lamentoso e hai un codice generico. È possibile utilizzare questa istanza per impostare manualmente le proprietà desiderate senza la necessità di un costruttore esplicito.
Bojo,

1

Non abbastanza rispondendo alla domanda, ma esiste una buona libreria per questo tipo di problemi: https://github.com/typestack/class-transformer (anche se non funzionerà per tipi generici, in quanto in realtà non esistono in fase di esecuzione (qui tutto il lavoro viene svolto con i nomi delle classi (che sono costruttori di classi)))

Per esempio:

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

1

Sono in ritardo per la festa, ma è così che ho funzionato. Per le matrici dobbiamo fare alcuni trucchi:

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

Usalo in questo modo:

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

Può essere migliorato ma ha funzionato per le mie esigenze!


1

Sto aggiungendo questo su richiesta, non perché penso che risolva direttamente la domanda. La mia soluzione prevede un componente tabella per la visualizzazione di tabelle dal mio database 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);
    }
}

Per usare questo, supponiamo che io abbia una vista (o una tabella) nel mio database VW_MyData e voglio colpire il costruttore della mia classe VW_MyData per ogni voce restituita da una query:

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

Il motivo per cui questo è desiderabile semplicemente assegnando il valore restituito a Data, è che ho un codice che applica una trasformazione a una colonna di VW_MyData nel suo costruttore:

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

Ciò mi consente di eseguire trasformazioni, convalide e qualsiasi altra cosa su tutti i miei dati che arrivano a TypeScript. Spero che fornisca alcune informazioni per qualcuno.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.