I try to implement Option/Maybe type with self map method in the instanced object.
To do that, firstly implementing the internal definition of none
and some
with the proper types, then outside definition as None
and Some
then finally, type Option<A> = None | Some<A>;
type Option<A> = any
Type alias 'Option' circularly references itself.(2456)
Surely, I understand what it suggests, but in TypeScript, it sometimes allow circular type definition, and either way, whatever the reason, I would like to know what is the typical approach or work-around to implement this pattern concisely.
Please advise.
CodePudding user response:
You're actually thinking a bit too functionally on this one. Speaking as a Haskell programmer, it's a good mindset, but you're in Typescript right now, so we need to think with classes.
class None {
constructor() {}
map(f: (a: never) => unknown): None {
return new None();
}
}
class Some<T> {
readonly value: T;
constructor(value: T) {
this.value = value;
}
map<S>(f: (a: T) => S): Some<S> {
return new Some(f(this.value));
}
}
type Option<T> = None | Some<T>;
const myFirstOption: Option<number> = new Some(1);
const mySecondOption: Option<number> = myFirstOption.map((x) => x 1);
console.log(mySecondOption);
const myNone: Option<number> = new None();
const myOtherNone: Option<number> = myNone.map((x) => x 1);
console.log(myOtherNone);
Union types in Typescript will infer the appropriate type for common methods, so Option<T>
will see that None
defines a map
which takes (a: never) => unknown
and Some<T>
defines map
which takes (a: T) => S
, so it'll combine the two.
I declared None.map
to take a function of type (a: never) => unknown
, since never
is the bottom type in Typescript and unknown
is the closest thing we have to a top type, so (a: never) => unknown
is a valid supertype of all one-argument functions, i.e. None.map
can be called with any single-argument function.
As suggested in the comments, here's the same code using interface
and tag fields rather than ES classes.
interface None {
_tag: "None";
}
interface Some<T> {
_tag: "Some";
readonly value: T;
}
type Option<T> = None | Some<T>;
function none(): None {
return { _tag: "None" };
}
function some<T>(value: T): Some<T> {
return { _tag: "Some", value };
}
function map<T, S>(f: (a: T) => S, opt: Option<T>): Option<S> {
if (opt._tag === "None") {
return opt;
} else {
return some(f(opt.value));
}
}
const myFirstOption: Option<number> = some(1);
const mySecondOption: Option<number> = map((x: number) => x 1, myFirstOption);
console.log(mySecondOption);
const myNone: Option<number> = none();
const myOtherNone: Option<number> = map((x: number) => x 1, myNone);
console.log(myOtherNone);
type
can be used to emulate interface
, so we could've replaced those top two definitions with
type None = {
_tag: "None";
}
type Some<T> = {
_tag: "Some";
readonly value: T;
}
and gotten the exact same result.