Вопрос или проблема
Рассмотрим следующие два класса:
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);
Пояснение к коду
-
Типизация контекста: Мы используем предусмотренный в TypeScript синтаксис для отображения типа. Мы создаем новый тип, который проецирует идентификаторы узлов в ключи и соответствующие типы результатов в значения.
-
Инициализация контекста: При создании экземпляра класса
Flow
, в конструкторе мы проходим по всем узлам и заполняемcontext
, обеспечивая, что каждыйid
узла становится ключом в контексте, а егоresult
— значением. -
Проверка типов: Благодаря такому подходу, TypeScript обеспечивает строгую проверку типов на уровне компиляции. Если вы попытаетесь обратиться к несуществующему ключу в
context
, TypeScript выдаст ошибку.
Заключение
Таким образом, используя возможности TypeScript, мы можем создать динамические типы для контекста в классе Flow
, которые соответствуют результатам действий узлов. Это позволяет сделать код более строгим и надежным, минимизируя вероятность ошибок при доступе к полям объекта.