Skip to content

Symbol.observable is missing from the Observable class on the type level #6777

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Andarist opened this issue Jan 21, 2022 · 2 comments · May be fixed by #6832
Closed

Symbol.observable is missing from the Observable class on the type level #6777

Andarist opened this issue Jan 21, 2022 · 2 comments · May be fixed by #6832

Comments

@Andarist
Copy link
Contributor

Describe the bug

RxJS can consume InteropObservables that implement Symbol.observable method but it doesn't allow for reverse - it doesn't make its own observables interop (at the type level) so they can't be consumed by other libs that would like to leverage Symbol.observable for this purpose.

Expected behavior

It should be possible to create a from method that would rely on the input argument having a Symbol.observable method and convert Rx observable to a different observable type.

Reproduction code

import { Observable, interval, InteropObservable, Subscribable } from "rxjs"; // types: 7.5.2

declare const obs1: Observable<number>;
const obs2 = interval(100);

type Test1 = typeof obs1 extends InteropObservable<infer T> ? T : never;
//   ^?
type Test2 = typeof obs2 extends InteropObservable<infer T> ? T : never;
//   ^?

declare class MyObservable<T> {
  subscribe(cb: (a: T) => void): void;
  // just make this class nominal to avoid relying on structural subtyping with the Observable from RxJS
  private tag: string;
}

// strategy based on the one in RxJS itself
// https://github.com/ReactiveX/rxjs/blob/5d0e41ee46337f56dba6d9a1ef938002b261ccac/src/internal/types.ts#L90
type ObservableInput<T> = MyObservable<T> | InteropObservable<T>;

// https://github.com/ReactiveX/rxjs/blob/5d0e41ee46337f56dba6d9a1ef938002b261ccac/src/internal/types.ts
type ObservedValueOf<O> = O extends ObservableInput<infer T> ? T : never;

// https://github.com/ReactiveX/rxjs/blob/888c753e272443bbb879f9ffb1a8145a7773c865/src/internal/observable/from.ts#L6
declare function from<O extends ObservableInput<any>>(
  input: O
): MyObservable<ObservedValueOf<O>>;

// actual
const converted1 = from(obs1);
const converted2 = from(obs2);

// expected result (without having to cast this)
const converted3 = from(obs1 as typeof obs1 & { [Symbol.observable]: () => Subscribable<number> });
//    ^?
const converted4 = from(obs2 as typeof obs2 & { [Symbol.observable]: () => Subscribable<number> });
//    ^?

Reproduction URL

https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgeQEYGcCmUBuBDVAG0wBo5gA7GbfQsgSSuwjDS1wOLIGUBXDAMZRgqTpjgBfOADMoEEHABEUAB4ArdIoDccAPS64MAJ5hM6AFxwA7ADoArDYBMAKGcATTAMJ4o4gRAp0eAgMAEZLNhoxAB4KXhBUbAA+LWd-QOCMRzgAXnImDkIAClCABlKASlTnY1M4ABUzGFDcwxNMCGk4EPQWzBVqCjd0OEZqOVYMKKJMaMppbAakuAB+BrhLCkwcbFT9OAOAPRWa9oam7Lzajq6e7P7B4dGClkiOGbmKBagl1fXN7a7Zz7I4ndyeby+OBePDoEYAWSMb3wH3qywQzgO6H46CEIkwRQEqEsRTwlnqFVyyxwEGAbgqlhpdNSB32al4QTgIDwAGtxDAABbAEYwuFwCjySh4QiGCBwPBMtxwXyEIyUADm3QocCCUF4AhgvCg0p1-FqGrgAHdgILDALxMixDI5AoAEoqABS3ExcDAwnw1EMeHVll1GtSElc+11eGo6qMcFEWCVATt4gC4kocHdXvIMCwhGkwIMApgMDAFn06ptAv4Nn8IF0rsweANwB2AA1dKoNLoiCFdHY3KVMAAWUKYMcANgAzDOrNI7FO3KJlwBOPAT6RrmcADnKjlQjinoQEAlbunQUAEuko4wo0t013QNnzAGIADJr0qnOqOmaMGAvAwNEaKtIi-7EKBywAD7POMrxTO8UFotU+yluWla6NWgp1g2TYtm2nbduo6B9oQA5DiO46TqOs7zouy6rm4G5bju+6lIex6nueN5Xjed7YA+hBPu0L75r+DpITsbgAGrSrwmDINI0TIMseTIHADyYEMIyQZggHAZ83y-Gs9QbOKgJQGhJZlhW5hVjWeHyARrYwO2mBdj2ZH9qgui7gFAhWHYM6YI4ViOKOo4zqgsW7lYa7btI0ioKEeC7qEo52HgVi5TOAi7lOdiXtet4FMJug9NMxC6LI8ivugn5TuCMJQtIvAUG2qZ1SAqlaQMOlPPphkgXgFBGEkSRFD6lBATAETOAycAQdJMTIpgckKUpKlqSkUYGG5vDSmkAScukOywBtLR5D1RQ9KEVQnRk0IBBd1BuJczryHdWSPcW-WmAaG3KmYvCEPARTWoKEDAXAAoKhaMByuenKCsKFRPWdr3YO9M6tLd93yiM1ydN0YRwAAZIgcAANrcEYCQQIQNhVchmAALokpSOTLHwgjCKIHxxAkySSI9IKgpj8DnTjG2jvjLo-eg2Swm0pik3clPU3TDOoEzLOrTMnNwEU3O8zieKC1BwuJFAywSOLBgHHAxzOEAA

Version

7.5.2

Environment

No response

Additional context

No response

@benlesh
Copy link
Member

benlesh commented Feb 4, 2022

Unfortunately, TypeScript doesn't really support this right now. If you export a unique symbol from a module, then try to use it in another module, the type system can't figure it out just yet. I know this is something the TypeScript team was looking into, but I don't think this has ever been resolved.

@cartant touches on that in his blog here.

Sorry for the inconvenience, but we can't fix this until TypeScript can somehow support it.

@benlesh benlesh closed this as completed Feb 4, 2022
@Andarist
Copy link
Contributor Author

Andarist commented Feb 5, 2022

In such a case... wouldn't it be better to agree on more versatile interop? I mean... a string property like $$observable would work almost as well as a symbol. I know there are some subtle semantic differences but most of the time they don't matter.

I also think that this can still be fixed on your side - this "only" requires some build-level shenanigans. I've, sort of, worked around this issue by post-processing runtime code with such Babel plugin:
https://github.com/statelyai/xstate/blob/8d375ac4bd551cf9e9b415d075c03f99b9b9cf3a/packages/core/rollup.config.js#L7-L32

This allows me to keep Symbol.observable method for the time when TS emits .d.ts but I strip it from the runtime and I only keep the "ponyfilled" method:
https://github.com/statelyai/xstate/blob/8d375ac4bd551cf9e9b415d075c03f99b9b9cf3a/packages/core/src/interpreter.ts#L1359-L1362

I understand that this is a little bit crazy but I think that the means justify the ends here because Rx observable should be compatible with the interop strategy proposed by it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants