Buat objek baru dari tipe parameter di kelas generik

144

Saya mencoba membuat objek tipe parameter baru di kelas generik saya. Di kelas saya View, saya memiliki 2 daftar objek bertipe generik yang dilewatkan sebagai parameter tipe, tetapi ketika saya mencoba membuatnya new TGridView(), TypeScript mengatakan:

Tidak dapat menemukan simbol 'TGridView

Ini kodenya:

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

Bisakah saya membuat objek dari tipe generik?

Javier Ros
sumber

Jawaban:

95

Karena JavaScript yang dikompilasi memiliki semua jenis informasi yang dihapus, Anda tidak dapat menggunakan T untuk membuat objek baru.

Anda bisa melakukan ini dengan cara non-generik dengan mengirimkan tipe ke konstruktor.

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

Anda dapat memperluas contoh ini menggunakan obat generik untuk mengencangkan jenis:

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

Untuk membuat objek baru dalam kode generik, Anda perlu merujuk ke tipe dengan fungsi konstruktornya. Jadi alih-alih menulis ini:

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

Anda perlu menulis ini:

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

var classA: ClassA = activator(ClassA);

Lihat pertanyaan ini: Inferensi Tipe Umum dengan Argumen Kelas

blorkfish
sumber
35
Agak terlambat (tapi berguna) komentar - jika konstruktor Anda mengambil args maka new()kebutuhan juga - atau yang lebih umum:type: {new(...args : any[]): T ;}
Rycochet
1
@Yoda IActivatable adalah antarmuka yang dibuat sendiri, lihat pertanyaan lain: stackoverflow.com/questions/24677592/…
Jamie
1
Bagaimana ini harus disimpulkan { new(): T ;}. Saya mengalami kesulitan untuk mempelajari bagian ini.
abitcode
1
Ini (dan semua solusi lain di sini) mengharuskan Anda untuk lulus di kelas selain menentukan generik. Ini adalah hacky dan mubazir. Itu berarti jika saya memiliki kelas dasar ClassA<T>dan memperluasnya ClassB extends ClassA<MyClass>, saya juga harus lulus new () => T = MyClassatau tidak ada solusi yang berfungsi.
AndrewBenjamin
@AndrewBenjamin Apa saran Anda? Bukan mencoba memulai sesuatu (sebenarnya ingin belajar) tetapi menyebutnya hacky dan redundant menyimpulkan Anda tahu cara lain yang tidak Anda bagikan :)
perry
37

Semua jenis informasi dihapus di sisi JavaScript dan oleh karena itu Anda tidak dapat membuat T baru seperti status @Sohnee, tapi saya lebih suka meminta parameter yang diketik diteruskan ke konstruktor:

class A {
}

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

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

  protected item = {} as T;
}

Objeknya akan dapat menerima parameter apa pun, namun, tipe T hanya merupakan referensi naskah dan tidak dapat dibuat melalui konstruktor. Artinya, itu tidak akan membuat objek kelas.

jales cardoso
sumber
6
Hati-hati , ini mungkin berfungsi tetapi objek yang dihasilkan tidak dari Tipe T, sehingga setiap getter / setter yang didefinisikan Tmungkin tidak berfungsi.
Daniel Ormeño
@ DanielOrmeño ingin menjelaskan? Bagaimana apanya? Kode saya tampaknya berfungsi menggunakan cuplikan ini. Terima kasih.
eestein
Javascript adalah bahasa yang ditafsirkan dan ecmascript belum memiliki referensi ke tipe atau kelas. Jadi T hanyalah referensi untuk naskah.
jales cardoso
Ini adalah jawaban yang SALAH, bahkan dalam JavaScript. Jika Anda memiliki class Foo{ method() { return true; }, casting {} ke T tidak akan memberi Anda metode ini!
JCKödel
8

Saya tahu terlambat tetapi jawaban @ TadasPa dapat disesuaikan sedikit dengan menggunakan

TCreator: new() => T

dari pada

TCreator: { new (): T; }

jadi hasilnya akan terlihat seperti ini

class A {
}

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

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

Saya sedang mencoba untuk instantiate generik dari dalam kelas dasar. Tak satu pun dari contoh di atas bekerja untuk saya karena mereka memerlukan jenis beton untuk memanggil metode pabrik.

Setelah meneliti sebentar tentang ini dan tidak dapat menemukan solusi online, saya menemukan bahwa ini tampaknya berhasil.

 protected activeRow: T = {} as T;

Potongan:

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

...

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

Bersama

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

Yang mengatakan, jika Anda membutuhkan contoh aktual Anda harus menggunakan:

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

Jika Anda memiliki argumen, Anda dapat menggunakan.

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
sumber
2
Ini adalah jawaban yang SALAH, bahkan dalam JavaScript. Jika Anda memiliki kelas Foo {method () {return true; }, casting {} ke T tidak akan memberi Anda metode ini!
JCKödel
Saya bekerja jika Anda tidak memiliki metode. Namun jika Anda memiliki objek dengan metode Anda harus menggunakan kode yang baru saja saya tambahkan.
Christian Patzer
2

Inilah yang saya lakukan untuk mempertahankan info jenis:

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

saya menggunakan ini: let instance = <T>{}; umumnya berfungsi 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
sumber
5
Ini tidak akan benar-benar instantiate instance baru T, itu hanya akan membuat objek kosong yang menurut TypeScript adalah tipe T. Anda mungkin ingin menjelaskan jawaban Anda sedikit lebih detail.
Sandy Gifford
Saya katakan itu umumnya bekerja. Persis seperti yang Anda katakan. Kompiler tidak merengek dan Anda memiliki kode generik. Anda dapat menggunakan contoh ini untuk secara manual mengatur properti yang Anda inginkan tanpa perlu konstruktor eksplisit.
Bojo
1

Tidak cukup menjawab pertanyaan, tapi, ada perpustakaan yang bagus untuk masalah seperti itu: https://github.com/typestack/class-transformer (walaupun itu tidak akan bekerja untuk tipe generik, karena mereka tidak benar-benar ada pada saat run-time (di sini semua pekerjaan dilakukan dengan nama kelas (yang merupakan konstruktor kelas)))

Misalnya:

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

Saya terlambat ke pesta, tapi ini cara saya membuatnya bekerja. Untuk array kita perlu melakukan beberapa trik:

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

Gunakan seperti ini:

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

Itu dapat ditingkatkan tetapi berfungsi untuk kebutuhan saya!

EQuadrado
sumber
1

Saya menambahkan ini dengan permintaan, bukan karena saya pikir itu langsung memecahkan pertanyaan. Solusi saya melibatkan komponen tabel untuk menampilkan tabel dari database SQL saya:

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

Untuk menggunakan ini, anggap saya memiliki tampilan (atau tabel) di basis data saya VW_MyData dan saya ingin mengenai konstruktor kelas VW_MyData saya untuk setiap entri yang dikembalikan dari kueri:

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

Alasan ini diinginkan daripada hanya memberikan nilai yang dikembalikan ke Data, adalah mengatakan saya punya beberapa kode yang menerapkan transformasi ke beberapa kolom VW_MyData dalam konstruktornya:

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

Ini memungkinkan saya untuk melakukan transformasi, validasi, dan apa pun pada semua data saya yang masuk ke TypeScript. Semoga ini memberikan wawasan bagi seseorang.

AndrewBenjamin
sumber