こんばんは。 @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>
を定義し、実際のmap
や get
の挙動も定義します。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