Динамический тип TypeScript, основанный на других полях в классе?

Вопрос или проблема

Рассмотрим следующие два класса:

export class Node<T extends (...args: any[]) => any> {
  id: string;
  action: T;
  result: ReturnType<T>;

  constructor(id: string, action: T) {
    this.id = id;
    this.action = action;
  }
}

export class Flow {
  nodes: Node<(flow: Flow) => any>[];

  // Как я могу создать здесь карту, которая принудит nodes[i].id быть единственными допустимыми строками ключами,
  // а nodes[i].result - соответствующим типом значения?
  context: <помощь>?

  constructor(nodes: Node<(flow: Flow) => any>[]) {
    this.nodes = nodes;
  }
}

Как я могу добавить поле контекста в класс Flow, чтобы следующий код работал, как указано?

const flow = new Flow([
  new Node("one", () => 1),
  new Node("two", (flow) => flow.context.one + 1),
]);

flow.context.one; // 1
flow.context.asdasd; // ошибка typescript, отсутствует в идентификаторах узлов

Ответ или решение

Чтобы создать контекст в классе Flow, который будет обеспечивать строгую типизацию для ключей и значений, основанных на полях экземпляров класса Node, мы можем воспользоваться возможностями TypeScript, такими как индексация типов и условные типы. Давайте рассмотрим, как можно это реализовать с учетом того, что nodes[i].id должен быть единственным допустимым ключом, а nodes[i].result — соответствующим значением.

Шаг 1: Определение интерфейса контекста

Сначала мы создадим интерфейс, который будет представлять контекст, обеспечивая соответствие между уникальными идентификаторами узлов и их результатами. Для этого мы воспользуемся утилитным типом Record, который позволяет создать объект с заданными ключами и соответствующими для них типами значений.

Шаг 2: Обновим класс Flow

Теперь мы можем интегрировать эту концепцию в наш класс Flow, добавив поле context, которое будет вычисляться на основе входящих узлов. При этом учтём, что значение, присвоенное каждому ключу, будет соответствовать типу результата соответствующего узла.

Вот как это может выглядеть:

export class Node<T extends (...args: any[]) => any> {
  id: string;
  action: T;
  result: ReturnType<T>;

  constructor(id: string, action: T) {
    this.id = id;
    this.action = action;
    this.result = this.action(); // предположим, что action вызывается в момент создания узла
  }
}

export class Flow {
  nodes: Node<(flow: Flow) => any>[];

  // Здесь мы определяем тип контекста на основе узлов
  context: {
    [K in Node<(flow: Flow) => any> as K['id']]: K['result'];
  };

  constructor(nodes: Node<(flow: Flow) => any>[]) {
    this.nodes = nodes;

    this.context = {} as { 
      [K in Node<(flow: Flow) => any> as K['id']]: K['result'];
    };

    for (const node of this.nodes) {
      this.context[node.id] = node.result; // Заполняем контекст
    }
  }
}

// Пример использования
const flow = new Flow([
  new Node("one", () => 1),
  new Node("two", (flow) => flow.context.one + 1),
]);

console.log(flow.context.one); // 1
// Консоль даст ошибку на следующей строке, если 'asdasd' отсутствует в узлах
// console.log(flow.context.asdasd);

Пояснение к коду

  1. Типизация контекста: Мы используем предусмотренный в TypeScript синтаксис для отображения типа. Мы создаем новый тип, который проецирует идентификаторы узлов в ключи и соответствующие типы результатов в значения.

  2. Инициализация контекста: При создании экземпляра класса Flow, в конструкторе мы проходим по всем узлам и заполняем context, обеспечивая, что каждый id узла становится ключом в контексте, а его result — значением.

  3. Проверка типов: Благодаря такому подходу, TypeScript обеспечивает строгую проверку типов на уровне компиляции. Если вы попытаетесь обратиться к несуществующему ключу в context, TypeScript выдаст ошибку.

Заключение

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

Оцените материал
Добавить комментарий

Капча загружается...