ghivert
ghivert

Reputation: 548

TypeScript Overload with different return types

As an exercise, I’m trying to reimplement the Maybe pattern from Haskell-like languages into TypeScript. So far so well, I got a satisfying result, and it’s working great, with some type checking stuff that I had to override by hand. I would like to be a little bit more JavaScript-ish and I try to define a function map which can return both pure data or Promise of data.

Here's some code to be more clear:

class Maybe<A> {
  #value: A | null

  private constructor(value: A) {
    this.#value = value
  }

  static just<A>(value: A): Maybe<A> {
    return new Maybe(value)
  }

  static nothing<A>(): Maybe<A> {
    return <Maybe<A>>(<unknown>new Maybe(null))
  }

  static nullable<A>(value: A | null): Maybe<A> {
    if (value) {
      return this.just(value)
    } else {
      return this.nothing()
    }
  }

  value(): A {
    if (this.#value) {
      return this.#value
    } else {
      throw new Error('Maybe has no data')
    }
  }

  map<B>(mapper: (a: A) => Promise<B>): Promise<Maybe<B>> // I got an error here in the overloading
  map<B>(mapper: (a: A) => B): Maybe<B> {
    if (this.#value) {
      const result = mapper(this.#value)
      if (result instanceof Promise) {
        return result.then((e: B) => Maybe.just(e))
      } else {
        return new Maybe(mapper(this.#value))
      }
    } else {
      return Maybe.nothing()
    }
  }
}

I was wondering if it is possible to define the map function as defined above?

Upvotes: 1

Views: 903

Answers (1)

Artyom Smirnov
Artyom Smirnov

Reputation: 366

I think you can do it like these, look at the comments

class Maybe<A> {
  #value: A | null

  // altered argument here
  private constructor(value: A | null) {
    this.#value = value
  }

  static just<A>(value: A): Maybe<A> {
    return new Maybe(value)
  }

  static nothing<A>(): Maybe<A> {
    // omitted casts
    return new Maybe<A>(null);
  }

  static nullable<A>(value: A | null): Maybe<A> {
    if (value) {
      return this.just(value)
    } else {
      return this.nothing()
    }
  }

  value(): A {
    if (this.#value) {
      return this.#value
    } else {
      throw new Error('Maybe has no data')
    }
  }

  // added two separate declarations
  map<B>(mapper: (a: A) => Promise<B>): Promise<Maybe<B>>;
  map<B>(mapper: (a: A) => B): Maybe<B>;

  // and single implementation which fits both declarations
  map<B>(mapper: (a: A) => B | Promise<B>): Maybe<B> | Promise<Maybe<B>> {
    if (this.#value) {
      const result = mapper(this.#value)
      if (result instanceof Promise) {
        return result.then((e: B) => Maybe.just(e))
      } else {
        return new Maybe(result)
      }
    } else {
      return Maybe.nothing()
    }
  }
}

So now Maybe.just(0).map(x=>x+1) is Maybe<number> but Maybe.just(0).map(x=>Promise.resolve(x+1)) is Promise<Maybe<number>>

However for Maybe.nothing().map(x=>Promise.resolve(x+1)) you'll receive Maybe<number> while interface will declare Promies<Maybe<number>> so it's not a good idea, since you can't tell function return type in runtime without calling it

Upvotes: 1

Related Questions