Создайте новый объект из параметра типа в универсальном классе

150

Я пытаюсь создать новый объект параметра типа в моем универсальном классе. В моем классе у Viewменя есть 2 списка объектов универсального типа, переданных в качестве параметров типа, но когда я пытаюсь создать new TGridView(), TypeScript говорит:

Не удалось найти символ TGridView

Это код:

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

Могу ли я создавать объекты из универсального типа?

Хавьер Рос
источник

Ответы:

97

Поскольку в скомпилированном коде JavaScript вся информация о типе удалена, вы не можете использовать его Tдля создания нового объекта.

Вы можете сделать это нетипичным способом, передав тип в конструктор.

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

Вы можете расширить этот пример, используя дженерики, чтобы сузить типы:

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();
Фентон
источник
148

Чтобы создать новый объект в универсальном коде, вам необходимо обратиться к типу с помощью его функции-конструктора. Поэтому вместо того, чтобы писать это:

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

Вам нужно написать это:

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

var classA: ClassA = activator(ClassA);

См. Этот вопрос: Вывод универсального типа с аргументом класса

морская рыба
источник
37
Несколько поздний (но полезный) комментарий - если ваш конструктор принимает аргументы, то это new()также необходимо - или более общий:type: {new(...args : any[]): T ;}
Rycochet
1
@Yoda IActivatable - это самостоятельно созданный интерфейс, см. Другой вопрос: stackoverflow.com/questions/24677592/…
Джейми
1
Как это сделать { new(): T ;}? Мне трудно выучить эту часть.
abitcode
1
Это (и все другие решения здесь) требуют, чтобы вы передали класс поверх указания универсального. Это взломано и избыточно. Это означает, что если у меня есть базовый класс ClassA<T>и я расширяю его ClassB extends ClassA<MyClass>, я также должен передать его, new () => T = MyClassиначе ни одно из этих решений не сработает.
AndrewBenjamin
@AndrewBenjamin Что вы тогда предлагаете? Не пытаюсь ничего начать (на самом деле хочу учиться), но называя это хакерским и избыточным, подразумевает, что вы знаете другой способ, которым вы не делитесь :)
Перри
38

Вся информация о типах стирается на стороне JavaScript, и поэтому вы не можете обновлять T, как @Sohnee, но я бы предпочел, чтобы типизированный параметр передавался в конструктор:

class A {
}

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

var test = new B<A>(A);
ТадасПа
источник
13
export abstract class formBase<T> extends baseClass {

  protected item = {} as T;
}

Его объект сможет получить любой параметр, однако тип T является всего лишь ссылкой на машинописный текст и не может быть создан через конструктор. То есть он не будет создавать никаких объектов класса.

Jales Cardoso
источник
6
Остерегайтесь , это может сработать, но результирующий объект не будет иметь тип T, поэтому любые геттеры / сеттеры, определенные в, Tмогут не работать.
Daniel Ormeño 05
@ DanielOrmeño не хочешь объяснить? Что ты имеешь в виду? Похоже, мой код работает с этим фрагментом. Спасибо.
eestein
Javascript - это интерпретируемый язык, а в ecmascript еще нет ссылок на типы или классы. Так что T - это просто ссылка на машинописный текст.
jales cardoso
Это НЕПРАВИЛЬНЫЙ ответ даже в JavaScript. Если у вас есть class Foo{ method() { return true; }, приведение {} к T не даст вам этого метода!
JCKödel
9

Я знаю поздно, но ответ @TadasPa можно немного изменить, используя

TCreator: new() => T

вместо того

TCreator: { new (): T; }

итак результат должен выглядеть так

class A {
}

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

var test = new B<A>(A);
Джазиб
источник
4

Я пытался создать общий экземпляр из базового класса. Ни один из приведенных выше примеров не помог мне, поскольку для вызова фабричного метода требовался конкретный тип.

После некоторого исследования этого и неспособного найти решение в Интернете, я обнаружил, что это, похоже, работает.

 protected activeRow: T = {} as T;

Кусочки:

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

...

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

Все вместе

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

Тем не менее, если вам нужен реальный экземпляр, вы должны использовать:

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

Если у вас есть аргументы, вы можете использовать.

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
Это НЕПРАВИЛЬНЫЙ ответ даже в JavaScript. Если у вас есть класс Foo {method () {return true; }, приведение {} к T не даст вам этого метода!
JCKödel
Работаю, если нет методов. Однако если у вас есть объект с методами, вы должны использовать только что добавленный мной код.
Christian Patzer
2

Вот что я делаю, чтобы сохранить информацию о типе:

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

Я использую это: let instance = <T>{}; обычно он работает 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
На самом деле это не будет создавать новый экземпляр T, а просто создаст пустой объект, который, по мнению TypeScript, имеет тип T. Возможно, вы захотите пояснить свой ответ более подробно.
Сэнди Гиффорд
Я сказал, что в целом работает. Все именно так, как вы говорите. Компилятор не скулит, и у вас есть общий код. Вы можете использовать этот экземпляр для ручной установки нужных свойств без необходимости явного конструктора.
Bojo
1

Не совсем отвечаю на вопрос, но для таких проблем есть хорошая библиотека: https://github.com/typestack/class-transformer (хотя для общих типов это не сработает, поскольку их на самом деле не существует во время выполнения (здесь вся работа выполняется с именами классов (которые являются конструкторами классов)))

Например:

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
источник
1

Я опаздываю на вечеринку, но так у меня получилось. Для массивов нам нужно проделать несколько хитростей:

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

Используйте это так:

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

Его можно улучшить, но он работает для моих нужд!

EQuadrado
источник
1

Я добавляю это по запросу, а не потому, что считаю, что это напрямую решает вопрос. Мое решение включает в себя табличный компонент для отображения таблиц из моей базы данных 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);
    }
}

Чтобы использовать это, предположим, что у меня есть представление (или таблица) в моей базе данных VW_MyData, и я хочу использовать конструктор моего класса VW_MyData для каждой записи, возвращаемой из запроса:

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

Причина, по которой это желательно вместо простого присвоения возвращаемого значения Data, заключается в том, что у меня есть код, который применяет преобразование к некоторому столбцу VW_MyData в его конструкторе:

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

Это позволяет мне выполнять преобразования, проверки и все остальное для всех моих данных, поступающих в TypeScript. Надеюсь, это поможет кому-то понять.

ЭндрюБенджамин
источник
0

Вот пример, если вам нужны параметры в конструкторе:

class Sample {
    public innerField: string;

    constructor(data: Partial<Sample>) {
        this.innerField = data.innerField;
    }
}

export class GenericWithParams<TType> {
    public innerItem: TType;

    constructor(data: Partial<GenericWithParams<TType>>, private typePrototype: new (i: Partial<TType>) => TType) {
        this.innerItem = this.factoryMethodOnModel(data.innerItem);
    }

    private factoryMethodOnModel = (item: Partial<TType>): TType => {
        return new this.typePrototype(item);
    };
}

const instance = new GenericWithParams<Sample>({ innerItem : { innerField: 'test' }}, Sample);
0lukasz0
источник