Operator<T, U> (type)

Operators provide a way to transform events data before they are passed to the callback.

EVT Operators can be of three types:

  • Filter: (data: T)=> boolean.

    Only the matched event data will be passed to the callback.

  • Type guard: <U extends T>(data: T)=> data is U

    Functionally equivalent to filter but restrict the event data type.

  • Filter / transform / detach handlers

    • Stateless fλ: <U>(data: T)=> [U] | null | "DETACH" | {DETACH:Ctx} |...

    • Stateful fλ: [ <U>(data: T, prev: U)=> ..., U ]

      Uses the previous matched event data transformation as input à la Array.prototype.reduce

Operator - Filter

Let us consider the example use of an operator that filters out every word that does not start with 'H'.

import { Evt } from "evt";

const evtText= Evt.create<string>();

evtText.attach(
    text=> text.startsWith("H"), 
    text=> {
        console.assert( text.startsWith("H") );
        console.log(text);
    }
);

//Nothing will be printed to the console.
evtText.post("Bonjour");

//"Hi!" will be printed to the console.
evtText.post("Hi!");

Run the example

It is important to be sure that your filter always return a boolean, typewise you will be warned it is not the case but you must be sure that it is actually the case at runtime. If in doubts use 'bang bang' ( !!returnedValue ). This note also applies for Type Gard operators.

Operator - Type guard

If you use a filter that is also a type guard, the type of the callback argument will be narrowed down to the matched type.

Let us define a straight forward type hierarchy to illustrate this feature.

The matchCircle type guard can be used to attach a callback to an Evt<Shape> that will only be called against circles.

The type of the Shape object is narrowed down to Circle Screenshot 2020-02-08 at 19 17 46

Run the example

Operator - fλ

Anonymous functions to simultaneously filter, transform the data and control the event flow.

fλ Returns

The type of values that a fλ operator sole determine what it does:

  • null If the event should be ignored and nothing passed to the callback.

  • [ U ] or [ U, null ] When the event should be handled, wrapped into the singleton is the value will be passed to the callback.

  • "DETACH" If the event should be ignored and the handler detached from the Evt

  • { DETACH: Ctx<void> } If the event should be ignored and a group of handlers bound to a certain context be detached. See Ctx<T>

  • { DETACH: Ctx<V>, res: V } See Ctx<T>``

  • { DETACH: Ctx; err: Error } See Ctx<T>``

  • [ U, "DETACH" ] / [ U, {DETACH:Ctx, ...} ] If the event should be handled AND some detach be performed.

Stateless fλ

Stateless fλ operator only takes the event data as arguments.

Other example using "DETACH"

Example use of [U,null|"DETACH"], handling the event that causes the handler to be detached.

Example use of { DETACH:Ctx}, detaching a group of handlers bound to a given context.

Run examples

Stateful fλ

The result of the previously matched event is passed as argument to the operator.

****Run the example****

Dos and don'ts

Operators cannot have any side effect (they cannot modify anything). No assumption should be made on when and how they are called.

Don't encapsulate state, do use stateful

The first thing that you might be tempted to do is to use a variable available in the operator's scope as an accumulator.

The following example seems equivalent from the previous one but it is not.

When evt.isHandled(data) is invoked the operator of every handler is invoked. The operator is invoked again when the event is actually posted.

In the example every time the operator is invoked the encapsulated variable acc is updated. This result in "Foo bar" being accumulated twice when the event is posted only once.

evt.postAsyncOnceHandled(data) will also cause dry invokations of the operators.

If state is needed stat full fλ have to be used.

Don't modify input, do return a copy.

Do use const assertions ( as const )

The TypeScript const assertion features come in handy if you introduce closures, for example. The following example does not compile without the use of as const.

Generally const assertions can help you narrow down the return type of your operator. In the following example without the const assertions data is inferred as being string | number , with the const assertions it is "TOO LARGE" | number

Do write single instruction function, try to avoid explicit return.

This is more a guideline than a requirement but you should favor data => expression over data=> { ...return x; } wherever possible for multiple reasons:

  1. It is much less likely to inadvertently produce a side effect writing a single expression function than it is writing a function with explicit returns.

  2. Operators are meant to be easily readable. If you think the operator you need is too complex to be clearly expressed by a single instruction, you should consider splitting it in multiple operators and using the compose function introduced in the next section.

  3. It is easier for TypeScript to infer the return type of single expression functions.

Here is the previous example using explicit returns just to show you that the return type has to be explicitly specified, this code does not copy without it.

compose(op1, op2, ..., opn)

opn...op2op1op_n \circ... \circ op_2 \circ op_1

Operators can be composed ( aka piped ) to achieve more complex behaviour.

For most use cases, it is more convenient to chain evt.pipe() calls rather than using compose. However it is very useful for creating custom operators.

Example composing type guards with fλ:

Example with on ( operator used to do things à la EventEmitter)

Example composing three fλ to count the number of different words in a sentence:

Using stateful fλ operators to implement throttleTime(duration), an operator that let through at most one event every duration milliseconds.

Run the example

****Run the example****

Explicitly using the type alias

The Operator type alias defines what functions qualify as a valid EVT operaor. The type can be used as a scaffolder to write fλ.

In Operator<T, U> , T design the type of the event data and U design the type of the data spitted out by the operator. For filters operator U=T.

****Run the example****

Generic operators built in

Some generic operators are provided in "evt/lib/util/genericOperators" such as scan, throttleTime or to but that's about it.

Where to use operators

Operators functions can be used with:

Last updated