Crie um novo objeto a partir do parâmetro type na classe genérica

144

Estou tentando criar um novo objeto de um parâmetro de tipo na minha classe genérica. Na minha classe View, tenho 2 listas de objetos do tipo genérico passados ​​como parâmetros de tipo, mas quando tento criar new TGridView(), o TypeScript diz:

Não foi possível encontrar o símbolo 'TGridView

Este é o 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;
        }
    }
}

Posso criar objetos de um tipo genérico?

Javier Ros
fonte

Respostas:

95

Como o JavaScript compilado possui todas as informações de tipo apagadas, você não pode usar Tpara atualizar um objeto.

Você pode fazer isso de maneira não genérica, passando o tipo para o construtor.

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

Você pode estender este exemplo usando genéricos para reforçar os 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
fonte
144

Para criar um novo objeto dentro do código genérico, você precisa se referir ao tipo por sua função construtora. Então, ao invés de escrever isso:

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

Você precisa escrever isto:

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

var classA: ClassA = activator(ClassA);

Veja esta pergunta: Inferência de tipo genérico com argumento de classe

peixe branco
fonte
35
Um pouco tarde (mas útil) comentário - se o seu construtor leva args então as new()necessidades de bem - ou um mais genérico:type: {new(...args : any[]): T ;}
Rycochet
1
@Yoda IActivatable é uma interface de auto-criado, ver outra pergunta: stackoverflow.com/questions/24677592/...
Jamie
1
Como isso deve ser inferido { new(): T ;}. Estou tendo dificuldades para aprender esta parte.
Abitcode
1
Isso (e todas as outras soluções aqui) exigem que você passe na classe além de especificar o genérico. Isso é hacky e redundante. Isso significa que se eu tiver uma classe base ClassA<T>e estendê-la ClassB extends ClassA<MyClass>, também tenho que passar new () => T = MyClassou nenhuma dessas soluções funciona.
AndrewBenjamin 10/01
@AndrewBenjamin Qual é a sua sugestão então? Não tentando iniciar nada (realmente quero aprender), mas chamá-lo de hacker e redundante infere que você sabe de outra maneira que não está compartilhando :)
perry
37

Todas as informações de tipo são apagadas no lado do JavaScript e, portanto, você não pode atualizar o T como os estados @Sohnee, mas eu preferiria ter digitado o parâmetro passado para o construtor:

class A {
}

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

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

  protected item = {} as T;
}

Seu objeto poderá receber qualquer parâmetro; no entanto, o tipo T é apenas uma referência de texto datilografado e não pode ser criado por meio de um construtor. Ou seja, ele não criará nenhum objeto de classe.

jales cardoso
fonte
6
Cuidado , isso pode funcionar, mas o objeto resultante não será do Tipo T, portanto, quaisquer getters / setters definidos em Tpodem não funcionar.
Daniel Ormeño
@ DanielOrmeño gostaria de explicar? O que você quer dizer com isso? Meu código parece estar funcionando usando esse trecho. Obrigado.
eestein
Javascript é uma linguagem interpretada e o ecmascript ainda não possui referências a tipos ou classes. Então T é apenas uma referência para texto datilografado.
Jales cardoso
Esta é uma resposta ERRADA, mesmo em JavaScript. Se você tiver um class Foo{ method() { return true; }, converter {} para T não fornecerá esse método!
JCKödel 26/04/19
8

Eu sei tarde, mas a resposta da @ TadasPa pode ser ajustada um pouco usando

TCreator: new() => T

ao invés de

TCreator: { new (): T; }

então o resultado deve ficar assim

class A {
}

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

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

Eu estava tentando instanciar o genérico de dentro de uma classe base. Nenhum dos exemplos acima funcionou para mim, pois exigia um tipo concreto para chamar o método de fábrica.

Depois de pesquisar por um tempo sobre isso e incapaz de encontrar uma solução on-line, descobri que isso parece funcionar.

 protected activeRow: T = {} as T;

Os pedaços:

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

Dito isto, se você precisar de uma instância real, deverá 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();

Se você tiver argumentos, poderá 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
fonte
2
Esta é uma resposta ERRADA, mesmo em JavaScript. Se você tem uma classe Foo {method () {return true; }, converter {} em T não fornecerá esse método!
JCKödel 26/04/19
Eu funciono se você não tiver métodos. No entanto, se você tiver um objeto com métodos, deverá usar o código que acabei de adicionar.
Christian Patzer
2

Isto é o que eu faço para manter as informações 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
fonte
1

Eu uso isso: let instance = <T>{}; geralmente funciona 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
fonte
5
Na verdade, isso não instancia uma nova instância T, apenas cria um objeto vazio que o TypeScript pensa que é do tipo T. Você pode explicar sua resposta com mais detalhes.
Sandy Gifford
Eu disse que geralmente funciona. É exatamente como você diz. O compilador não está reclamando e você tem código genérico. Você pode usar esta instância para definir manualmente as propriedades desejadas sem a necessidade de construtor explícito.
Bojo 4/17
1

Ainda não estou respondendo à pergunta, mas há uma boa biblioteca para esse tipo de problema: https://github.com/typestack/class-transformer (embora não funcione para tipos genéricos, pois eles realmente não existem em tempo de execução (aqui todo o trabalho é feito com nomes de classes (que são construtores de classes)))

Por exemplo:

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

Estou atrasado para a festa, mas foi assim que consegui. Para matrizes, precisamos fazer alguns truques:

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

Use-o assim:

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

Pode ser melhorado, mas funcionou para as minhas necessidades!

EQuadrado
fonte
1

Estou adicionando isso por solicitação, não porque acho que resolve diretamente a questão. Minha solução envolve um componente de tabela para exibir tabelas do meu banco de dados 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 colocar isso em uso, suponha que eu tenha uma visualização (ou tabela) no meu banco de dados VW_MyData e desejo atingir o construtor da minha classe VW_MyData para cada entrada retornada de uma 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);
            }
        }
    }
}

A razão pela qual isso é desejável ao simplesmente atribuir o valor retornado a Data, é dizer que eu tenho algum código que aplica uma transformação a alguma coluna de VW_MyData em seu construtor:

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

Isso me permite realizar transformações, validações e tudo o mais em todos os meus dados que chegam ao TypeScript. Espero que ele forneça algumas dicas para alguém.

AndrewBenjamin
fonte