bussorenre Laboratory

hoge piyo foo bar

TypeScript で Option っぽいものを作ってみる

こんばんは。 @bussorenre です。趣味でTypeScript をはじめました。

Option っぽいもの

Scala には、値が無いことを許容する仕組みとして Option という仕組みがあります。Some(A) は値がある状態。 None は値がない状態を示します。 中の値を取り出すには、get というメソッドを用いますが、None に対してget を実行してしまうとエラーになってしまします。

値があるか無いかわからないけど中の値を操作したい時、 map を使います。

以下に例を示します。

val a: Option[Int] = Some(100) // or None

// a が Some でも None でも実行できる
a.map(x => x + 100)

a match {
    case Some(a) => println(a)    // 200
    case None => println("None")  // None
}

これと似たような物をTypeScript で再現します。あくまで「Option っぽいもの」なので、厳密にはOption ではありません。

案1: 共用体を使う

最初に思いついた方法です。共用体を使います。共用体は代入可能型を絞ることができます。

let a: number | null = 100
a = null // 問題なし
a = "hogehogehogehoge" // エラーになる

これを用いて、nullable な変数は全部 A | null にしてしまえばいいなじゃないかと思いつきます。しかし、毎回値が有効か、null かをif 文でチェックする必要があり、面倒です。

let a: number | null = 100 // or null
if ( a === null) {
    // error
}
console.log(a)

案2: もうちょっといい感じに共用体を使う。

interface を用いて、Some型とNone 型を定義し、それらを type で 共用型にします

interface Some<A> {
  type: 'some';
  value: A;
}
interface None {
  type: 'none';
}
type Option<A> = Some<A> | None;

これは比較的良さそうです。Option<A>Some<A>None を持ちません。しかし、Option.map みたいな芸当ができません。無理やりmap 関数を作るとすればこんな感じになりそうです。

function map<A, B>(obj: Option<A>, f: (obj: A)=> B): Option<B> {
  if (obj.type === 'some') {
    return {
      type: 'some',
      value: f(obj.value),
    };
  } else {
    return {
      type: 'none',
    };
  }
}

案3: 素直にクラスを使ってみる。

Option<A> というインターフェースを用意し、そこから派生させたインターフェース Some<A>None を定義します。また、それぞれ Some<A> を継承したクラスSome<A>を定義し、実際のmapget の挙動も定義します。None も同様です。

interface Option<A> {
    get(): A | null
    map<B>(f: (a: A) => B): Option<B>
}

interface Some<A> extends Option<A> {
    value: A
    get(): A
}

interface None extends Option<null> {
    get(): null
}

class Some<A> implements Some<A> {
    constructor(value: A) {
        this.value = value
    }
    get(): A {
        return this.value
    }
    map<B>(f: (a: A) => B): Some<B> {
        return new Some(f(this.value))
    }
}

class None implements None {
    constructor() {
    }

    get(): null {
        return null
    }

    map<A, B>(f: (a: A) => B): None {
        return new None
    }
}


let a: Option<number> = new Some(100)
console.log(a.map( x => x + 100))    // Some(200)

let b: Option<number> = new None
console.log(b.map( x => x + 100))    // None


console.log(new Some(100).map(x => x + 100))    // 200
console.log(new None().map( (x: number) => x + 100))    // new None.map はできない。x の型を明示しないとコンパイルエラー

get ができて、map が出来る、Option っぽいものができました。めでたしめでたし。

最後に

決り文句的で恐縮ですが、「もっとこうしたらいい」とか「ここ間違っているよ」等があれば、ご指摘いただけると幸いです。

技術書典7で定価よりお安く頂いた「実践TypeScript - BFF と Next.js & Nuxt.js の型定義 吉井健文著」が非常にわかりやすくて勉強になっています。良書をありがとうございます。

追記

プロトタイプを使うことにより、更にScala のOption っぽいものに近づけることが出来るみたいです。勉強になりました…。 https://github.com/AlexGalays/spacelift/blob/master/src/option/index.ts