Перейти к содержимому
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, чтобы связать ключ с конкретным типом результата.