Home > Mobile >  What is the proper approach to implement `type Option<A> = None | Some<A>` without an er
What is the proper approach to implement `type Option<A> = None | Some<A>` without an er

Time:03-17

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>;

enter image description here

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.

  • Related