はじめに

僕もお前らの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もいいぞ
bannerAds