Как получить Generi c Inference с помощью шаблона команд в TypeScript? - PullRequest
0 голосов
/ 05 августа 2020

Мой вариант использования - это движок Condition. Каждый объект JS представляет собой тип условия, которое я должен оценить, или ошибку, если оно не поддерживается.

Упрощенный код ниже работает так, как в нем выполняется, и записывает true в консоль по мере необходимости. Проблемы в основном связаны с набором текста и логическим выводом:

  1. this.actions[name] = command; вызывает ошибку набора машинописного текста из-за используемых мною обобщений: Type 'Command<D, Out>' is not assignable to type 'Command<CommandData, Out>'.
  2. Клиент не получает ввод для объект данных, несмотря на указание имени Condition, мы зарегистрировали парой строк выше.

Непосредственно на площадке TypeScript.

Код библиотеки

Шина не должна ничего знать об именах команд, поскольку они динамически добавляются клиентом. Он должен ввести аргумент execute метода data на основе аргумента name (полученного с помощью метода add, ранее использовавшегося клиентом).

// Used internally
interface CommandData {
    [key: string]: any
}

// Externally, the same with a `name` property to identify which Command to call
interface NamedCommandData extends CommandData {
    name: string
}

type Command<In extends CommandData, Out> = (data: In, bus: CommandBus<Out>) => Promise<Out>;

type CommandRegistry<In extends CommandData, Out> = Record<string, Command<In, Out>>

class CommandBus<Out> {
    public actions: CommandRegistry<CommandData, Out> = {};

    add<D extends CommandData, Out>(name: string, command: Command<D, Out>) {
        // I need to use "@ts-ignore" here otherwise it moans:

        // Type 'Command<D, Out>' is not assignable to type 'Command<CommandData, Out>'. 
        // Types of parameters 'data' and 'data' are incompatible. 
        // Type 'CommandData' is not assignable to type 'D'.
        // 'CommandData' is assignable to the constraint of type 'D', but 'D' could be instantiated with a different subtype of constraint 'CommandData'.
        
        this.actions[name] = command;
    }

    async execute(name: string, data: CommandData) : Promise<Out> {
        return this.actions[name](data, this);
    }

    getNameFromData(data: NamedCommandData | any): string | null {
        if (!data || !data.name) return null
        if (!this.actions[data.name]) return null
        return (data as NamedCommandData).name
    }
}

Общая структура:

  • Command - интерфейс, представляющий обработчик заданного c типа команды; каждый получает свои данные и шину (чтобы иметь возможность обрабатывать дочерние элементы, если они есть)
    • EveryCommand - получено EveryCommandData, чтобы получить логическое ожидаемое значение и дочерние команды для оценки
    • StringEqualsCommand - получает StringEqualsCommandData, которые представляют собой две строки (data.left и data.right).
  • CommandData - это простой интерфейс, в котором расширяются все входные данные конкретных команд. NamedCommandData версия publi c с дополнительным name: string для идентификации правого обработчика (например, name: "StringEquals").
  • CommandBus - это точка входа для клиентов. Они могут добавлять свои команды, а также выполнять их.
  • CommandRegistry - это место, где имена команд и обработчики хранятся для поиска и выполнения.

Код клиента

  1. Определяет свои собственные команды (props + handler)
  2. Регистрирует их на шине
  3. Execute
// Command: check equality between two strings

interface StringEqCommandData extends CommandData {
    left: string
    right: string
}

const StringEqCommand: Command<StringEqCommandData, boolean> = async (data) => {
    return data.left === data.right
}

// Command: reduce children command executions and check equality against the expected value

interface EveryCommandData extends CommandData {
    commands: CommandData[]
    value: boolean
}

const EveryCommand: Command<EveryCommandData, boolean> = async (data, bus) => {
    
    const expectedValue = data.value
    const childrenCommands = data.commands

    const results = await Promise.all(
        childrenCommands.map((child) => {
            const name = bus.getNameFromData(child)
            return name ? bus.execute(name, child) : Promise.resolve(false)
        })
    )

    for (const result of results) {
        if (result !== expectedValue)
            return false
    }

    return true 
}

// Instantiate the bus and register the Commands
const bus = new CommandBus<boolean>()
bus.add(`Every`, EveryCommand)
bus.add(`StringEquals`, StringEqCommand)

// /!\ There is no typing here for the data (second arg), based on the name (first arg)
const execution = bus.execute(`Every`, {
    value: true,
    commands: [
        {
            name: `StringEquals`,
            left: `abc`,
            right: `abc`,
        },
        {
            name: `StringEquals`,
            left: `123`,
            right: `123`,
        },
    ]
})

execution
    .then(console.log)
    .catch(console.error)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...