Strategies for Object Creation from Type Parameters in TypeScript Generic Classes

Nov 22, 2025 · Programming · 12 views · 7.8

Keywords: TypeScript | Generics | Object_Creation

Abstract: This article explores the challenges and solutions for creating objects from type parameters in TypeScript generic classes. Due to type erasure during compilation to JavaScript, direct use of new T() syntax results in compilation errors. By analyzing best practices, the paper introduces methods such as passing constructor parameters and using factory functions to ensure type safety while enabling flexible object instantiation. With code examples, it explains how to design generic classes for dynamic type creation and discusses alternatives like type inference and reflection.

Introduction

In TypeScript development, generics provide powerful type abstraction, allowing for reusable components. However, when attempting to create objects from type parameters within generic classes, developers often encounter compilation errors, such as "Could not find symbol 'T'". Based on real-world Q&A data, this article examines the root causes of this issue and presents effective solutions.

Problem Analysis

TypeScript generics are erased during compilation, meaning type information is not retained in the resulting JavaScript. Consequently, directly using new T() syntax fails at runtime because type T cannot be resolved. For example, in a generic class View<TFormView, TGridView>, calling new TGridView() triggers an error, as TGridView exists only for compile-time type checking.

Core Solution: Passing Constructor Parameters

To address this, the best practice is to pass constructors as parameters to the generic class. This approach leverages TypeScript's type system to ensure safety. Here is an improved example:

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

// Usage example
var test = new TestTwo<TestSub>(TestSub);
var example = test.getNew();
example.hi(); // Outputs: "Hi from sub"

In this code, the TestTwo class accepts a constructor parameter testType of type new () => T, representing a parameterless constructor that returns an instance of T. By using new this.testType(), objects can be dynamically created at runtime while maintaining type constraints.

Extended Application: Factory Function Pattern

Drawing from practices in Kotlin, factory functions offer a more flexible approach to object creation. In TypeScript, function parameters can be used similarly to support complex construction logic:

class CellList<T extends Cell> {
    private cells: T[] = [];

    constructor(num: number, factory: (index: number) => T) {
        for (let i = 0; i < num; i++) {
            this.cells.push(factory(i));
        }
    }
}

// Usage example
interface Cell { }
class SomeCell implements Cell { }

const list = new CellList<SomeCell>(10, (index) => new SomeCell());

This method not only supports default constructors but also allows passing indices or other parameters, enhancing code extensibility. The factory function pattern is particularly useful for custom initialization logic.

Type Safety and Error Handling

When implementing generic object creation, type compatibility must be ensured. Using extends constraints on generic parameters prevents runtime type errors. For instance, in TestTwo<T extends TestBase>, T must be a subclass of TestBase, guaranteeing that objects returned by getNew have the hi method.

Additionally, complex mechanisms like reflection should be avoided unless necessary. TypeScript's compile-time type checking suffices for most scenarios, reducing runtime overhead.

Practical Case Improvement

Based on the original Q&A's View class, we can refactor it to support safe object creation:

module AppFW {
    export class View<TFormView extends FormView, TGridView extends GridView> {
        public Forms: { [idForm: string]: TFormView; } = {};
        public Grids: { [idForm: string]: TGridView; } = {};

        constructor(
            private formViewConstructor: new (element: HTMLFormElement, dataModel: any, submitFunction?: (e: SubmitFormViewEvent) => boolean) => TFormView,
            private gridViewConstructor: new (element: HTMLDivElement, gridOptions: any) => TGridView
        ) { }

        public AddForm(formElement: HTMLFormElement, dataModel: any, submitFunction?: (e: SubmitFormViewEvent) => boolean): TFormView {
            var newForm: TFormView = new this.formViewConstructor(formElement, dataModel, submitFunction);
            this.Forms[formElement.id] = newForm;
            return newForm;
        }

        public AddGrid(element: HTMLDivElement, gridOptions: any): TGridView {
            var newGrid: TGridView = new this.gridViewConstructor(element, gridOptions);
            this.Grids[element.id] = newGrid;
            return newGrid;
        }
    }
}

In this improvement, the View class receives constructors for TFormView and TGridView in its constructor, enabling safe instantiation in the AddForm and AddGrid methods. This eliminates compilation errors and preserves type precision.

Conclusion

Creating objects from type parameters in TypeScript generic classes is not feasible with direct new T() due to type erasure. By passing constructor parameters or factory functions, type-safe dynamic instantiation can be achieved. The methods discussed in this article, supported by code examples, aid developers in designing maintainable generic components. Future work could explore TypeScript decorators or advanced type features to further optimize object creation processes.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.