Cree un nuevo objeto a partir del parámetro de tipo en la clase genérica

144

Estoy tratando de crear un nuevo objeto de un parámetro de tipo en mi clase genérica. En mi clase View, tengo 2 listas de objetos de tipo genérico pasados ​​como parámetros de tipo, pero cuando intento hacer new TGridView(), TypeScript dice:

No se pudo encontrar el símbolo 'TGridView

Este es el código:

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

¿Puedo crear objetos de un tipo genérico?

Javier Ros
fuente

Respuestas:

95

Debido a que el JavaScript compilado tiene toda la información de tipo borrada, no puede usar Tpara renovar un objeto.

Puede hacer esto de una manera no genérica pasando el tipo al constructor.

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

Puede ampliar este ejemplo utilizando genéricos para ajustar los tipos:

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
fuente
144

Para crear un nuevo objeto dentro del código genérico, debe referirse al tipo por su función constructora. Entonces, en lugar de escribir esto:

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

Necesitas escribir esto:

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

var classA: ClassA = activator(ClassA);

Ver esta pregunta: Inferencia de tipo genérico con argumento de clase

blorkfish
fuente
35
Comentario algo tardío (pero útil): si su constructor toma argumentos, entonces también lo new()necesita, o un comentario más genérico:type: {new(...args : any[]): T ;}
Rycochet
1
@Yoda IActivatable es una interfaz de creación propia, vea otra pregunta: stackoverflow.com/questions/24677592/…
Jamie
1
¿Cómo debería inferirse esto { new(): T ;}? Me está costando aprender esta parte.
abitcode
1
Esta (y todas las otras soluciones aquí) requieren que pase la clase además de especificar el genérico. Esto es hacky y redundante. Significa que si tengo una clase base ClassA<T>y la extiendo ClassB extends ClassA<MyClass>, también tengo que pasar new () => T = MyClasso ninguna de estas soluciones funciona.
AndrewBenjamin
@AndrewBenjamin ¿Cuál es tu sugerencia entonces? No estoy tratando de comenzar nada (en realidad quiero aprender), pero llamándolo infierno hacky y redundante, sabes de otra manera que no estás compartiendo :)
Perry
37

Toda la información de tipo se borra en el lado de JavaScript y, por lo tanto, no puede renovar T como en los estados de @Sohnee, pero preferiría que el parámetro mecanografiado se pasara al constructor:

class A {
}

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

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

  protected item = {} as T;
}

Su objeto podrá recibir cualquier parámetro, sin embargo, el tipo T es solo una referencia mecanografiada y no se puede crear a través de un constructor. Es decir, no creará ningún objeto de clase.

jales cardoso
fuente
66
Tenga cuidado , esto puede funcionar, pero el objeto resultante no será de Tipo T, por lo que los captadores / definidores definidos en Tpueden no funcionar.
Daniel Ormeño
@ DanielOrmeño me importa explicar? ¿Qué quieres decir con eso? Parece que mi código funciona con este fragmento. Gracias.
eestein
Javascript es un lenguaje interpretado y ecmascript aún no tiene referencias a tipos o clases. Entonces, T es solo una referencia para el mecanografiado.
jales cardoso
Esta es una respuesta INCORRECTA, incluso en JavaScript. Si tienes un class Foo{ method() { return true; }, ¡lanzar {} a T no te dará este método!
JCKödel
8

Lo sé tarde, pero la respuesta de @ TadasPa se puede ajustar un poco usando

TCreator: new() => T

en vez de

TCreator: { new (): T; }

así que el resultado debería verse así

class A {
}

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

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

Estaba tratando de crear una instancia del genérico desde una clase base. Ninguno de los ejemplos anteriores funcionó para mí, ya que requerían un tipo concreto para llamar al método de fábrica.

Después de investigar por un tiempo sobre esto e incapaz de encontrar una solución en línea, descubrí que esto parece funcionar.

 protected activeRow: T = {} as T;

Las piezas:

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

...

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

Todos juntos

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

Dicho esto, si necesita una instancia real, debe usar:

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 tiene argumentos, puede usar.

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
fuente
2
Esta es una respuesta INCORRECTA, incluso en JavaScript. Si tiene una clase Foo {method () {return true; }, ¡lanzar {} a T no te dará este método!
JCKödel
Yo trabajo si no tienes métodos. Sin embargo, si tiene un objeto con métodos, debe usar el código que acabo de agregar.
Christian Patzer
2

Esto es lo que hago para conservar la información de 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);

});
Saturno
fuente
1

Yo uso esto: let instance = <T>{}; generalmente funciona EDITAR 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
fuente
55
En realidad, esto no creará una instancia nueva T, solo creará un objeto vacío que TypeScript cree que es de tipo T. Es posible que desee explicar su respuesta con un poco más de detalle.
Sandy Gifford
Dije que generalmente funciona. Es exactamente como tú dices. El compilador no está quejándose y tienes un código genérico. Puede usar esta instancia para establecer manualmente las propiedades que desee sin la necesidad de un constructor explícito.
Bojo
1

No estoy respondiendo la pregunta, pero hay una buena biblioteca para ese tipo de problemas: https://github.com/typestack/class-transformer (aunque no funcionará para tipos genéricos, ya que realmente no existen en tiempo de ejecución (aquí todo el trabajo se realiza con nombres de clase (que son constructores de clases)))

Por ejemplo:

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
fuente
1

Llego tarde a la fiesta, pero así es como lo hice funcionar. Para las matrices necesitamos hacer algunos trucos:

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

Úselo así:

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

¡Se puede mejorar pero funcionó para mis necesidades!

EQuadrado
fuente
1

Estoy agregando esto por solicitud, no porque piense que resuelve directamente la pregunta. Mi solución implica un componente de tabla para mostrar tablas de mi base de datos 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);
    }
}

Para poner esto en uso, suponga que tengo una vista (o tabla) en mi base de datos VW_MyData y quiero golpear el constructor de mi clase VW_MyData para cada entrada devuelta de una consulta:

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 razón por la que esto es deseable en lugar de simplemente asignar el valor devuelto a Datos, es decir que tengo un código que aplica una transformación a alguna columna de VW_MyData en su constructor:

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

Esto me permite realizar transformaciones, validaciones y cualquier otra cosa en todos mis datos que ingresan a TypeScript. Esperemos que proporcione alguna idea para alguien.

AndrewBenjamin
fuente