16 / 19
Как типизировать функцию-фабрику с generic?
Полный ответ
Самая простая фабрика принимает класс и возвращает его экземпляр:
TypeScript
// Базовая фабрика — принимает конструктор без аргументов
function create<T>(Ctor: new () => T): T {
return new Ctor();
}
class User {
name = "Alice";
}
class Product {
title = "Laptop";
}
const user = create(User); // тип: User
const product = create(Product); // тип: Product
console.log(user.name); // "Alice"
console.log(product.title); // "Laptop"
Когда конструктору нужны аргументы, generic расширяется:
TypeScript
// Фабрика с аргументами конструктора
function createWith<T, A extends unknown[]>(
Ctor: new (...args: A) => T,
...args: A
): T {
return new Ctor(...args);
}
class Order {
constructor(
public id: number,
public amount: number
) {}
}
const order = createWith(Order, 1, 99.99); // тип: Order
// createWith(Order, "abc"); // ошибка: string не подходит для number
Фабрика с конфигурацией вместо конструктора:
TypeScript
// Фабрика через конфиг-объект
interface WidgetConfig<T> {
type: string;
build: (config: Record<string, unknown>) => T;
}
function createWidget<T>(widget: WidgetConfig<T>, config: Record<string, unknown>): T {
return widget.build(config);
}
interface Chart {
render(): void;
}
const chartFactory: WidgetConfig<Chart> = {
type: "chart",
build: (config) => ({
render() {
console.log("Rendering chart", config);
},
}),
};
const chart = createWidget(chartFactory, { data: [1, 2, 3] }); // тип: Chart
chart.render();
Фабрика-реестр для нескольких классов:
TypeScript
// Реестр фабрик с discriminated map
interface ShapeMap {
circle: { radius: number };
rect: { width: number; height: number };
}
function createShape<K extends keyof ShapeMap>(
kind: K,
params: ShapeMap[K]
): ShapeMap[K] & { kind: K } {
return { ...params, kind };
}
const circle = createShape("circle", { radius: 10 }); // тип: { radius: number } & { kind: "circle" }
const rect = createShape("rect", { width: 5, height: 3 }); // тип: { width: number; height: number } & { kind: "rect" }
// createShape("circle", { width: 5 }); // ошибка: width не существует в { radius: number }
Реальные кейсы
DI-контейнер с типобезопасным resolve:
TypeScript
class Container {
private registry = new Map<string, new (...args: never[]) => unknown>();
register<T>(key: string, Ctor: new () => T): void {
this.registry.set(key, Ctor as new (...args: never[]) => unknown);
}
resolve<T>(key: string): T {
const Ctor = this.registry.get(key);
if (!Ctor) throw new Error(`Not registered: ${key}`);
return new Ctor() as T;
}
}
Типичная ошибка — потеря типа из-за Function:
TypeScript
// ❌ Нет типизации возвращаемого значения
function badFactory(Ctor: Function) {
return Ctor();
}
// ✅ Generic сохраняет тип
function goodFactory<T>(Ctor: new () => T): T {
return new Ctor();
}
Резюме
Фабрика с generic строится вокруг конструкторной сигнатуры new (...args: A) => T. Это позволяет TypeScript вывести тип возвращаемого экземпляра автоматически. Для фабрик на основе конфигурации используйте mapped types или discriminated map, чтобы связать ключ с конкретным типом результата.