はじめに
僕もお前らのXXXXは<ネガティブな形容詞>シリーズ をマネしたかったんです。
ごめんなさい。
Typescript書いていますか?
あなたはundefined派ですか?
null+undefined派ですか?
throw new Error派ですか?
return {error:null, result:true}派ですか?
nullとundefinedがないコードを書いてみましょう。
なんでnullを使ったらいけないの?
例えば、こういう例
//初期値としてnullを使う
let str:string = null;
//何か処理をしてstrに格納...できてなかった
console.log(str.toLowerCase()); //TypeError: Cannot read property 'toLowerCase' of null
nullがハンドリングできていないのでtoLowerCase()でエラーになります。
tsconfig.jsonに “strict”:true や “strictNullChecks”:trueをつけておくと、この例はコンパイルが通らないので、まず有効にしましょう。
nullではなく、undefinedにすると、コンパイラが警告してくれますし、Optional chainingなどもできるようになります。
let str:string|undefined = undefined;
console.log(str.toLowerCase()); //コンパイルエラー strは必ずundefinedだから
let str:string|undefined = undefined;
if(Math.random() > 0.5){ //特に意味のない、コンパイルを通すための処理
str = 'yey';
}
console.log(str?.toLowerCase()); //strがundefinedだったら、toLowerCase()が実行されない
でも、null or undefinedは統一されていない
document.querySelector("abc"); //null
[1,2,3].find(v=>v===4); //undefined
[1,2,3].filter(v=>v===4); //[]
綺麗事だけじゃ生きていけない。
undefinedに統一してみる・・・こんな場合はどうする?
//ダウンロードしたファイルの中身を文字列で返す。
function downloadSomething(url:string): string | undefined{
//ダウンロード失敗したらundefinedを返す?
//複数のエラーの切り分けをどうする?
//throw new Errorして外側のtry-catchでハンドリングする?
}
エラーハンドリングをどうするか、困ることが多いのではないでしょうか。
このように書く方法もあります。
function validate(value: number): {error?: string} {
if (value < 0 || value > 100) return {error:'Invalid value'};
}
↑Typescript deep diveでは、こういう書き方を紹介しています。
でも、戻り値が必要だと {error?: string, result?:string}のようになってきて、
チームが大きいと統一が難しくなってきます。
RustのOptionとResultはいいぞ
Rustでは、言語としてこういった問題に対処するためのデータ構造を用意しています。
OptionはSomeかNoneという値を取る
nullやundefinedを使わないコードが書ける
ResultはOkかErrという値を取る
失敗する可能性がある関数の戻り値をハンドリングできる
RustのOptionとResultをTypescriptにポーティングしました。
https://github.com/komasayuki/ts-rust
Typescript deep diveの ”ひどい関数の例”で考えてみます。
function toInt(str:string) {
return str ? parseInt(str) : undefined;
}
引数のstrはnullを取る可能性がある。
これをts-rustを使ってRustっぽく書くには
toInt(str:string) を toInt(str:Option)と宣言する。
また、戻り値はundefinedのケースがあります。
そのため、元の戻り値は number|undefinedでした。
この戻り値はResult<int, string>と宣言します。
書き直すと
import {RustOption as Option, RustResult as Result} from 'ts-rust'
//簡単な例
function toIntVersion1(str:Option<string>) :Result<number, string> {
if(str.isSome()){
return Result.Ok(str.unwrap());
}
return Result.Err('parse failed');
}
//よりRustっぽい例
function toIntVersion2(str:Option<string>) :Result<number, string> {
//str.map(parseInt) strがSomeならparseIntに渡す、strがNoneならそのままNoneを返す
//okOr() 左辺がOption.SomeならResult.Ok(左辺)を返す、左辺がOption.NoneならResult.Err(引数)を返す
return str.map(parseInt).okOr('parse failed');
}
console.log(toIntVersion2(Option.Some(234)).toString()); //Ok(234)
console.log(toIntVersion2(Option.None)+''); //Err("parse failed")
toIntVersion1やtoIntVersion2のようになります。
※最初の例から、そもそもparseIntのNaNをハンドリングしていないことは無視して下さい!
新しく作らなくてもNullableな型は提供してるライブラリはあるよね
あったんですが、いくつかの理由で作り直すことにしました。
-
- RustのOption/Resultの全機能を素直にポーティングしているものがなかった
-
- Rustはsnake_caseだけど、TypescriptではcamelCaseにしたかった
-
- デバッグするときに優しくなかった
- 重要なベースになるライブラリなので、外部依存のない小さいライブラリにしたかった
Rustはいいぞ
- Typescriptもいいぞ