A Lazy Sequence

A generic function to forward events

Here’s a neat function made possible with recent TypeScript releases (2.8 onward):

interface IEventSink<T> {
    register(f: (v: T)=>void): void;
    raise(v: T): void;
}

type ForwardableEventSinks<TSrc, TDest> = {
    [K in keyof (TSrc & TDest)]: 
        (TSrc & TDest)[K] extends IEventSink<infer V>
            ? IEventSink<V>
            : never;
}

function forwardOne<TSrc, TDest>(src: ForwardableEventSinks<TSrc, TDest>, 
    dest: ForwardableEventSinks<TSrc, TDest>,
    eventName: keyof ForwardableEventSinks<TSrc, TDest>
): void {
    src[eventName].register(v => dest[eventName].raise(v));
}

forwardOne1 takes two objects of different types that expose the same event sink (same name, same type argument), and makes connects the source to the destination. This is really handy when building components by composition.

The trick here is in the utility type ForwardableEventSinks. This combines three features of the type system – mapped types, intersection types, and conditional types – to find the common subtype of the TSrc and TDest that contains only the exposed event sinks where they match.

The only thing here that’s a little non-obvious is the specific way the intersection is described: [K in keyof (TSrc & TDest)]: (TSrc & TDest)[K] … Note here that on both sides of the mapping the type in question is the same intersection. While we know that any value in keyof (TSrc & TDest) is going to be in both keyof TSrc and keyof TDest (that’s basically the definition of intersection after all), the type checker wants to be a bit fussier there and I was finding it would raise an error if eg I tried to do TSrc[K]. Maybe this is a bug? I’m not smart enough to figure it out.

Conveniently this particularly formulation of the type proved to not only be correct, and checkable, but also the tidiest version of the idea I could express.

  1. This is trivial to then extend to a second version – say forwardAll – that takes an array of event names.
4 September 2018