TypeScriptで関数型の要素を取り入れてプログラミングしていると、switch文に値を返してほしくなったりPipeline operatorやResult
型が使いたくなってきます。
fp-tsなどの関数型プログラミング用ライブラリを使うのも一つの手ですが、素のTypeScriptから離れすぎていて学習コストが高そうなので、最低限の機能だけを持ったLiftsというライブラリを作ってみました。
Liftsは「LIghtweight Functional programming library for TypeScript」の略です。
インストール方法
yarn add lifts
# or
npm i -S lifts
Do
Do
は関数の即時実行を短く書くための関数です。ECMAScriptで策定中のdo式を元にしています。
import { Do } from 'lifts'
const result = Do(() => {
const x = 1
const y = 2
return x + y
})
console.log(result) // => 3
Switch
Switch
はswitch文をオブジェクトスタイルで書くための関数です。値を返すこともできます。
import { Switch } from 'lifts'
const lang = 'en' as string
const result = Switch(lang)(
{
ja: () => {
return '日本語'
},
en: () => {
return '英語'
},
},
() => null, // デフォルト値
)
console.log(result) // => '英語'
引数がリテラル型だと網羅性チェックもしてくれるので、switch文でありがちなcaseの書き忘れを防ぐことができます。
Pipe
P
P
はECMAScriptで策定中のPipeline operatorもどきです。
そもそもPipeline operator |>
は左辺の値を引数として右辺の関数を呼び出す演算子で、以下のように連ねると一番左の値に対して関数を順に適用することができます。
const mul2 = (n: number) => n * 2
const add1 = (n: number) => n + 1
const result = 3 |> mul2 |> add1 // add1(mul2(3)) と同じ; 3 * 2 + 1 = 7
Pipeline operatorはまだTypeScriptで使うことができないので、その動作を再現したのが**P
**です。
import { P } from 'lifts'
const result = P(3, mul2, add1)
配列などを操作する場合は**Remeda**を使うのがおすすめです。
import * as R from 'remeda'
import { P } from 'lifts'
const result = P([0, 1, 2], R.map(n => n * 2)) // => [0, 2, 4]
PP
PP
はP
のasyncバージョンです。
P
では関数がPromiseを返した場合、次の関数にそのPromiseがそのまま渡ってしまいますが、PP
では内部でawait
してから次の関数に渡すようになっています。
import { PP } from 'lifts'
const mul2Async = async (n: number) => n * 2
const add1 = (n: number) => n + 1
const result = await P(3, mul2Async, add1) // add1 には Promise<number> が渡るのでコンパイルエラー
const result = await PP(3, mul2Async, add1) // add1 には number が渡るのでOK
Result
Result
はRustやGoのようにエラーを戻り値で返して処理するための機能です。
処理が成功した場合は**Result.ok()
を使ってOk<T>
型を返し、失敗した場合はResult.err()
でErr<E>
**型を返すようにします。
const fn = (): IResult<T, E> => {
if (condition) {
return Result.ok(value)
} else {
return Result.err(error)
}
}
型定義はこのようになっています。 (実際にはOk
とErr
はクラスです)
type IResult<T, E> = Ok<T> | Err<E>
type Ok<T> = {
isOk: true
value: T
valueOrError: T
}
type Err<E> = {
isOk: false
error: E
valueOrError: E
}
- 日付の文字列をパースして、その日付が有効なら**
Ok
型で日付を、無効ならErr
**型でエラーを返す例
import { Result } from 'lifts'
const parseDate = (dateStr: string): IResult<Date, Error> => {
const date = new Date(dateStr)
// 日付が有効かチェック
if (!isNaN(date.valueOf())) {
return Result.ok(date)
} else {
return Result.err(new Error('Invalid Date'))
}
}
console.log(parseDate('2020-04-17'))
// => { isOk: true, value: Date('2020-04-17'), valueOrError: Date('2020-04-17') }
console.log(parseDate('foo'))
// => { isOk: false, error: Error('Invalid Date'), valueOrError: Error('Invalid Date') }
- 上の
parseDate
の戻り値が**Ok
型ならその日付のDay of monthを返し、Err
**型ならnull
を返す例
const result = parseDate('2020-04-17')
const dayOfMonth = Do(() => {
if (result.isOk) {
const { value: date } = result
return date.getDate()
} else {
const { error } = result
console.error(error)
return null
}
})
console.log(dayOfMonth) // => 17
Result.wrap
Result.wrap
はエラーをthrowするかもしれない関数をラップして、Ok
型またはErr
型の値で返すための関数です。
const wrappedResult = Result.wrap(() => {
if (condition) {
return value
} else {
throw new Error()
}
})
Liftsの機能は以上です。
そういえば1年ぐらい前にfp-tsを使ってTrySafeというResult.wrap
と同じような機能のライブラリを作ってましたがLiftsの方がシンプルで使いやすいと思います。
ぜひ使ってみてください👌