Reputation: 548
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
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