balu
balu

Reputation: 3831

TypeScript generics: Inequivalence of generic function type and call signature of an object literal type (Type 'T' is not assignable to type 'T')

I'm writing an RxJS operator which waits for the input operator to complete and then does a switchMap to a second operator (which is generated on the fly just as with switchMap). I have come up with two versions of my code, one which works and one which doesn't and I'm struggling to wrap my head around why that is.

The version that works:

import { Observable } from "rxjs"; // OperatorFunction,
import { defaultIfEmpty, last, switchMap } from "rxjs/operators";

// This definition of OperatorFunction is more or less equivalent to the
// definition in rxjs/src/internal/types.ts
interface OperatorFunction<T, S> {
    (input: Observable<T>): Observable<S>;
}

interface ObservableGenerator<T, S> {
    (value: T): Observable<S>;
}

export function switchMapComplete<T, S>(project: ObservableGenerator<T, S>): OperatorFunction<T, S> {
    function mapper(obs1: Observable<T>): Observable<S> {
        return obs1.pipe(
            defaultIfEmpty(null),
            last(),
            switchMap(project)
        );
    }

    return mapper;
}

The version that doesn't work (note that the only thing that changed are the definitions of OperatorFunction and OperatorGenerator):

import { Observable } from "rxjs";
import { defaultIfEmpty, last, switchMap } from "rxjs/operators";

type OperatorFunction2<T, S> = <T, S>(obs: Observable<T>) => Observable<S>;

type ObservableGenerator2<T, S> = <T, S>(value: T) => Observable<S>;

export function switchMapComplete2<T, S>(project: ObservableGenerator2<T, S>): OperatorFunction2<T, S> {
    function mapper(obs1: Observable<T>): Observable<S> {
        return obs1.pipe(
            defaultIfEmpty(null),
            last(),
            switchMap(project)
        );
    }

    return mapper;
}

The latter version causes the compiler to throw the following exception:

error TS2322: Type 'Observable<{}>' is not assignable to type 'Observable<S>'.
  Type '{}' is not assignable to type 'S'.
util.ts(49,5): error TS2322: Type '(obs1: Observable<T>) => Observable<S>' is not assignable to type 'OperatorFunction2<T, S>'.
  Types of parameters 'obs1' and 'obs' are incompatible.
    Type 'Observable<T>' is not assignable to type 'Observable<T>'. Two different types with this name exist, but they are unrelated.
      Type 'T' is not assignable to type 'T'. Two different types with this name exist, but they are unrelated.

I am really surprised by this and it took me ages to come up with the working version as the TypeScript docs say that both versions should be equivalent (as far as I understand).

I'd be grateful for any pointers as to why in my case the equivalence breaks down.

PS: For anyone in need of an RxJS operator similar to mine, here's another (working) solution which is slightly simpler and makes full use of the types that RxJS already provides:

import { Observable, ObservableInput, OperatorFunction, pipe } from "rxjs";
import { defaultIfEmpty, last, switchMap } from "rxjs/operators";

export function switchMapComplete<T, S>(project: (value: T) => ObservableInput<S>): OperatorFunction<T, S> {
    return pipe(
        defaultIfEmpty(null),
        last(),
        switchMap(project)
    );
}

Upvotes: 1

Views: 686

Answers (2)

artem
artem

Reputation: 51609

The generic function type in TypeScript is usually written as

type OperatorFunction2<T, S> = (obs: Observable<T>) => Observable<S>;

The syntax

type OperatorFunction2 = <T, S>(obs: Observable<T>) => Observable<S>;

where generic parameters appear "inline" in the type definition is also valid, but for some reason is not quite equivalent to the type definition where generic parameters are given explicitly for the type. This second form is sometimes useful when you have to provide an explicit type in a type annotation nested in another definition somewhere, when there is no type definition to attach generic parameters to.

When you combine both

type OperatorFunction2<T, S> = <T, S>(obs: Observable<T>) => Observable<S>;

then as jcalz noted you get two independent sets of generic parameters with the same names T and S, not related to each other.

For your example, you can just use the first syntax:

import { Observable } from "rxjs";
import { defaultIfEmpty, last, switchMap } from "rxjs/operators";

type OperatorFunction2<T, S> = (obs: Observable<T>) => Observable<S>;

type ObservableGenerator2<T, S> = (value: T) => Observable<S>;

export function switchMapComplete2<T, S>(project: ObservableGenerator2<T, S>): OperatorFunction2<T, S> {
    function mapper(obs1: Observable<T>): Observable<S> {
        return obs1.pipe(
            defaultIfEmpty(null),
            last(),
            switchMap(project)
        );
    }

    return mapper;
}

Upvotes: 2

jcalz
jcalz

Reputation: 327964

First off, you should change type OperatorFunction2<T, S> = <T, S>(obs: Observable<T>) => Observable<S> to just type OperatorFunction2 = <T, S>(obs: Observable<T>) => Observable<S> because you are not using the outer T or S in the definition of the type alias. The <T, S> in the inside shadows the outer names. And make the analogous change to ObservableGenerator2.

Note that type F<T> = (x:T) => void is not equivalent to type G = <T>(x:T)=>void. TypeScript doesn't allow fully generic values. Type F is generic and refers to a concrete function, and F must be given a type parameter to be used (F bad, F<string> good). Type G is a concrete type that refers to a generic function, and G cannot be given a type parameter (G<string> bad, G good). A value of type F<string> is concrete and can only accept string function inputs. A value of type G is generic and can accept any input.

It's not that the types are completely unrelated; they're just not equivalent. I wish I could help you more but I don't have RxJS installed anywhere, so the following code may still have errors. Still, I'll show you what I'd be trying here:

// concrete types referring to generic functions
type OperatorFunction2 = <T, S>(obs: Observable<T>) => Observable<S>;
type ObservableGenerator2 = <T, S>(value: T) => Observable<S>;

// a concrete function which takes a generic function and returns a generic function
export function switchMapComplete2(project: ObservableGenerator2): OperatorFunction2 {
  // a generic function
  function mapper<T, S>(obs1: Observable<T>): Observable<S> {
    return obs1.pipe(
      defaultIfEmpty(null),
      last(),
      switchMap(project)
    );
  }

  return mapper;
}

Hope that puts you on the right track. You may need to go down into the implementation of mapper and fix more signatures. Anyway, good luck!

Upvotes: 4

Related Questions