Créer un nouvel objet à partir du paramètre de type dans la classe générique

145

J'essaye de créer un nouvel objet d'un paramètre de type dans ma classe générique. Dans ma classe View, j'ai 2 listes d'objets de type générique passés comme paramètres de type, mais quand j'essaye de faire new TGridView(), TypeScript dit:

Impossible de trouver le symbole «TGridView

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

Puis-je créer des objets à partir d'un type générique?

Javier Ros
la source

Réponses:

96

Étant donné que le JavaScript compilé a toutes les informations de type effacées, vous ne pouvez pas utiliser Tpour créer un objet.

Vous pouvez le faire de manière non générique en passant le type dans le constructeur.

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

Vous pouvez étendre cet exemple en utilisant des génériques pour resserrer les types:

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
la source
144

Pour créer un nouvel objet dans du code générique, vous devez faire référence au type par sa fonction constructeur. Donc, au lieu d'écrire ceci:

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

Vous devez écrire ceci:

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

var classA: ClassA = activator(ClassA);

Voir cette question: Inférence de type générique avec argument de classe

poisson blork
la source
35
Commentaire un peu tardif (mais utile) - si votre constructeur prend des arguments, alors il le new()faut aussi - ou un commentaire plus générique:type: {new(...args : any[]): T ;}
Rycochet
1
@Yoda IActivatable est une interface auto-créée, voir autre question: stackoverflow.com/questions/24677592/…
Jamie
1
Comment cela devrait-il être déduit { new(): T ;}? J'ai du mal à apprendre cette partie.
abitcode
1
Ceci (et toutes les autres solutions ici) nécessitent que vous passiez dans la classe en plus de spécifier le générique. C'est hacky et redondant. Cela signifie que si j'ai une classe de base ClassA<T>et que ClassB extends ClassA<MyClass>je l' étend , je dois également passer new () => T = MyClassou aucune de ces solutions ne fonctionne.
AndrewBenjamin
@AndrewBenjamin Quelle est votre suggestion alors? Ne pas essayer de commencer quoi que ce soit (vraiment vouloir apprendre) mais l'appeler hacky et redondant sous-entend que vous connaissez une autre façon que vous ne partagez pas :)
perry
37

Toutes les informations de type sont effacées du côté JavaScript et vous ne pouvez donc pas créer de nouveaux T comme les états @Sohnee, mais je préférerais que le paramètre tapé soit passé au constructeur:

class A {
}

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

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

  protected item = {} as T;
}

Son objet pourra recevoir n'importe quel paramètre, cependant, le type T n'est qu'une référence dactylographiée et ne peut pas être créé via un constructeur. Autrement dit, il ne créera aucun objet de classe.

jales cardoso
la source
6
Attention , cela peut fonctionner mais l'objet résultant ne sera pas de Type T, donc les getters / setters définis dans Tpeuvent ne pas fonctionner.
Daniel Ormeño
@ DanielOrmeño voulez-vous expliquer? Que veux-tu dire par là? Mon code semble fonctionner en utilisant cet extrait. Merci.
eestein
Javascript est un langage interprété et ecmascript n'a pas encore de références aux types ou classes. Donc T est juste une référence pour dactylographié.
jales cardoso
C'est une mauvaise réponse, même en JavaScript. Si vous avez un class Foo{ method() { return true; }, lancer {} en T ne vous donnera pas cette méthode!
JCKödel
9

Je sais tard mais la réponse de @ TadasPa peut être légèrement ajustée en utilisant

TCreator: new() => T

au lieu de

TCreator: { new (): T; }

donc le résultat devrait ressembler à ceci

class A {
}

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

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

J'essayais d'instancier le générique à partir d'une classe de base. Aucun des exemples ci-dessus n'a fonctionné pour moi car ils nécessitaient un type concret pour appeler la méthode d'usine.

Après avoir fait des recherches pendant un certain temps à ce sujet et incapable de trouver une solution en ligne, j'ai découvert que cela semble fonctionner.

 protected activeRow: T = {} as T;

Les morceaux:

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

...

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

Tous ensemble

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

Cela dit, si vous avez besoin d'une instance réelle, vous devez utiliser:

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

Si vous avez des arguments, vous pouvez utiliser.

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
la source
2
C'est une mauvaise réponse, même en JavaScript. Si vous avez une classe Foo {method () {return true; }, lancer {} en T ne vous donnera pas cette méthode!
JCKödel
Je travaille si vous n'avez pas de méthodes. Si vous avez cependant un objet avec des méthodes, vous devez utiliser le code que je viens d'ajouter.
Christian Patzer
2

Voici ce que je fais pour conserver les informations de type:

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
la source
1

j'utilise ceci: let instance = <T>{}; cela fonctionne généralement 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
la source
5
Cela n'instanciera pas réellement une nouvelle instance de T, cela créera simplement un objet vide que TypeScript pense être de type T. Vous voudrez peut-être expliquer votre réponse un peu plus en détail.
Sandy Gifford
J'ai dit que cela fonctionne généralement. C'est exactement ce que vous dites. Le compilateur ne se plaint pas et vous avez du code générique. Vous pouvez utiliser cette instance pour définir manuellement les propriétés de votre choix sans avoir besoin d'un constructeur explicite.
Bojo
1

Je ne répond pas tout à fait à la question, mais il existe une belle bibliothèque pour ce genre de problèmes: https://github.com/typestack/class-transformer (bien que cela ne fonctionnera pas pour les types génériques, car ils n'existent pas vraiment au moment de l'exécution (ici tout le travail est fait avec les noms de classes (qui sont des constructeurs de classes)))

Par exemple:

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
la source
1

Je suis en retard pour la fête mais c'est ainsi que je l'ai fait fonctionner. Pour les tableaux, nous devons faire quelques astuces:

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

Utilisez-le comme ceci:

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

Il peut être amélioré mais a fonctionné pour mes besoins!

EQuadrado
la source
1

J'ajoute cela sur demande, pas parce que je pense que cela résout directement la question. Ma solution implique un composant de table pour afficher les tables de ma base de données 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);
    }
}

Pour mettre cela à profit, supposons que j'ai une vue (ou une table) dans ma base de données VW_MyData et que je veux frapper le constructeur de ma classe VW_MyData pour chaque entrée renvoyée par une requête:

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

La raison pour laquelle cela est souhaitable plutôt que d'attribuer simplement la valeur retournée à Data, c'est que j'ai du code qui applique une transformation à une colonne de VW_MyData dans son constructeur:

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

Cela me permet d'effectuer des transformations, des validations et tout le reste sur toutes mes données entrant dans TypeScript. Espérons que cela donne un aperçu à quelqu'un.

AndrewBenjamin
la source