Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

What is the proper approach to implement `type Option<A> = None | Some<A>` without an error: Type alias 'Option' circularly references itself

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

Playground Link

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

const obj = (() => {
  //-----------------------------------------
  // (a -> b) -> a -> b
  type pipe = <A, B>
    (f: (a: A) => B) => (a: A) => B;
  const pipe: pipe = f => a => f(a);
  //-----------------------------------------
  type none = {
    map: <A, B>(f: (a: A) => B) => none
  };
  const none: none = {
    map: f => none
  };
  type some = <A>(a: A) => {
    value: A,
    map: <A, B>(
      this: A,
      f: (a: A) => B
    ) => B
  };
  const some: some = (a) => ({
    value: a,
    map: function (this, f) { return pipe(f)(this); }
  });
  return { none,  some };
})();
//-----------------------------------------
type Option<A> = None | Some<A>
type None = Option<never>;
type Some<A> = (a: A) => Option<A>;
const None: None = obj.none;
const Some: Some = obj.some;
export {  None,  Some }

So far, I simply have done this, but the errors are:

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.

>Solution :

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.

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading