Skip to content

Event type guards #777

@steve-taylor

Description

@steve-taylor

The following properties could be used to narrow down event types:

  • hasValue
  • isNext
  • isInitial
  • isError
  • isEnd

Rather than this:

myStream$.subscribe(event => {
    if (event.hasValue) {
        const {prop1, prop2} = (event as Value<{prop1: string, prop2: boolean}>).value
    } else if (event.isError) {
        const {error} = (event as Error)
    }
})

it would be nice to do this instead:

myStream$.subscribe(event => {
    if (event.hasValue) {
        const {prop1, prop2} = event.value
    } else if (event.isError) {
        const {error} = event
    }
})

Here's some working skeleton code (only implements the distinguishing features of various event types):

interface INextEvent<V> {
    readonly hasValue: true
    readonly isNext: true
    readonly isInitial: false
    readonly isError: false
    readonly isEnd: false
    readonly value: V
}

interface IInitialEvent<V> {
    readonly hasValue: true
    readonly isNext: false
    readonly isInitial: true
    readonly isError: false
    readonly isEnd: false
    readonly value: V
}

interface IErrorEvent {
    readonly hasValue: false
    readonly isNext: false
    readonly isInitial: false
    readonly isError: true
    readonly isEnd: false
    readonly error: unknown
}

interface IEndEvent {
    readonly hasValue: false
    readonly isNext: false
    readonly isInitial: false
    readonly isError: false
    readonly isEnd: true
}

type IEvent<V> = INextEvent<V> | IInitialEvent<V> | IErrorEvent | IEndEvent

abstract class AbstractEvent<
    HasValue extends boolean,
    IsNext extends boolean,
    IsInitial extends boolean,
    IsError extends boolean,
    IsEnd extends boolean
> {
    constructor(
        public readonly hasValue: HasValue,
        public readonly isNext: IsNext,
        public readonly isInitial: IsInitial,
        public readonly isError: IsError,
        public readonly isEnd: IsEnd
    ) {}
}

class NextEvent<V> extends AbstractEvent<true, true, false, false, false> implements INextEvent<V> {
    constructor(public readonly value: V) {
        super(true, true, false, false, false)
    }
}

class InitialEvent<V> extends AbstractEvent<true, false, true, false, false> implements IInitialEvent<V> {
    constructor(public readonly value: V) {
        super(true, false, true, false, false)
    }
}

class ErrorEvent extends AbstractEvent<false, false, false, true, false> implements IErrorEvent {
    constructor(public readonly error: unknown) {
        super(false, false, false, true, false)
    }
}

class EndEvent extends AbstractEvent<false, false, false, false, true> implements IEndEvent {
    constructor() {
        super(false, false, false, false, true)
    }
}

The key is to declare events as being of type IEvent<V>. Although not all event types have a value, the nice part about this is that when either hasValue, isNext or isInitial are true, value doesn't need to be cast to V.

My own Bacon alternative (work in progress) doesn't use event classes at all. Events are created by factories and there's no need for weird prefixed type names such as IEvent.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions