TypeScript observable object with Symbol.observable ponyfill

Viewed 654

I'm trying to type an object that is observable and uses a custom Symbol.observable ponyfill (similar to the system-observable ponyfill; code shown below), but I can't figure out a way to do it.

I've tried using typeof $$observable for the key and tried making Symbol.observable a unique symbol, but neither of those worked. I've had to resort to casting the object, but the problem with that approach is that I then have to cast the entire object (not shown below) and that makes it harder to catch other type errors due to other properties in the object being incorrectly typed. I also can't just do ReturnType<typeof makeObservableContainer> because I'd like to have it defined in a separate interface without having to depend on the function.

I would alternatively be interested in knowing if there's a way to cast it on a more fine-grained level than casting the entire object (if it turns out to be impossible to do without casting).

I've provided a playground link. Any help would be appreciated.

First attempt (no casting)

Error message

Property '[Symbol.observable]' is missing in type '{ [x: string]: () => { [x: string]: ((observer: Observer) => { unsubscribe(): void; }) | (() => { [x: string]: ((observer: Observer) => { unsubscribe(): void; }) | ...; subscribe(observer: Observer): { ...; }; }); subscribe(observer: Observer): { ...; }; }; }' but required in type 'Container'.(2741)

Code

// system-observable.ts
declare global {
  interface SymbolConstructor {
    readonly observable: symbol
  }
}

const $$observable = /* #__PURE__ */ (() =>
  (typeof Symbol === 'function' && Symbol.observable) || '@@observable')()

export default $$observable

// observer-creator.ts
interface Observer {
    next(): void;
}

interface Observable {
    subscribe: (observer: Observer) => { unsubscribe(): void };
    [Symbol.observable](): Observable;
}

interface Container {
    // How do I type this?
    [Symbol.observable](): Observable;
}

function makeObservableContainer(): Container {
    // error here because this is not assignable to `Container`
    return {
        [$$observable]() {
            return {
                subscribe(observer: Observer) {
                    // TODO
                    return {
                        unsubscribe() {
                            // TODO
                        }
                    }
                },
                [$$observable]() { return this }
            }
        },

        // other properties that should conform to `Container` interface
    }
}

Playground Link: Provided


Second attempt (unique symbol and a little bit of casting)

Error

Type '() => { subscribe(observer: Observer): { unsubscribe(): void; }; observable: Observable; }' is not assignable to type '() => Observable'. Property '[Symbol.observable]' is missing in type '{ subscribe(observer: Observer): { unsubscribe(): void; }; observable: Observable; }' but required in type 'Observable'.(2322)

Code

// system-observable.ts
declare global {
  interface SymbolConstructor {
    readonly observable: unique symbol
  }
}

const $$observable: SymbolConstructor['observable'] = /* #__PURE__ */ (() =>
  (typeof Symbol === 'function' && Symbol.observable) || '@@observable')() as SymbolConstructor['observable']

export default $$observable

// observer-creator.ts
interface Observer {
    next(): void;
}

interface Observable {
    subscribe: (observer: Observer) => { unsubscribe(): void };
    [Symbol.observable](): Observable;
}

interface Container {
    // How do I type this?
    [Symbol.observable](): Observable;
}

function makeObservableContainer(): Container {
    // error here because this is not assignable to `Container`
    return {
        [$$observable]() {
            return {
                subscribe(observer: Observer) {
                    // TODO
                    return {
                        unsubscribe() {
                            // TODO
                        }
                    }
                },
                [$$observable]() { return this }
            }
        },

        // other properties that should conform to `Container` interface
    }
}

Playground Link: Provided

1 Answers

if you cast the "@@observable" string to any, you can type the $$observable as a unique symbol, which will let you use it as a key for the interfaces:

const $$observable: unique symbol = /* #__PURE__ */ (() =>
    (typeof Symbol === 'function' && Symbol.observable) || ('@@observable' as any))();

export default $$observable

// observer-creator.ts
interface Observer {
    next(): void;
}

interface Observable {
    subscribe: (observer: Observer) => { unsubscribe(): void };
    [$$observable](): Observable;
}

interface Container {
    [$$observable](): Observable;
}
Related