0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

TypeScriptの幽霊型、その2

Posted at

TypeScriptの幽霊型、その2

幽霊型とは、変数・関数内に使用されないまま、追加情報を提供する型である。用例として、型の不明なデータを確認するデコーダ(解析機)とともに使うと、デコードした型を幽霊型としてデコーダに保管できる。デコードしたデータを変えても、その元の形は型に残る。

前回の記事で紹介したように、幽霊型とデコーダを使い、元々anyunknown型のオブジェクトからネストされたフィールドの型までTypeScriptに伝えることができた。

しかし、今までのデコーダでは入力と出力がいつも同一型だったが、今回紹介するmapデコーダは違う。配列のmap同様、デコーダのmapはデコードしたデータに変更を加えるために用いるので、出力は入力と異なる値になる。

デコーダとは何か

まずデコーダとは何か復習しましょう。一番単純なデコーダは、TypeScriptの基本的な型(数字や文字列など)をデコードするだけ。

// Result => 結果は値かエラーになることを示す型
// Error => 汎用的なエラー型およびエラーの値
type Decoder<Output, Input = Output> = (input: unknown) => Result<Output, Error>

Decoder<Output, Input = Output>の場合、InputOutputは大抵同じ型になると予測し、デフォルトでInputOutputを代入する。したがって、Decoder<string>Decoder<string, string>に等しい。

// `ok`と`err`はそれぞれデータとエラーを含む`Result`のコンストラクタ
const stringDecoder: Decoder<string> = (input: unknown) => typeof input === "string" ? ok(input) : err('should be a string')

要は、typeofを利用し、入力は文字列だということが判明した。

mapデコーダ

では、文字列をデコードしたが、その文字列を数字に変換したいとする。いよいよ、mapの出番だ。

第1問

デコードされた文字列を数字に変換するデコーダだとすれば、mapはどの引数二つをもらわねばならない?

`map`の引数
  1. 文字列デコーダ
  2. 文字列から数字への変換を行う関数(例えば、Number関数)

そのmapデコーダに文字列を渡せば数字(それともエラー)が戻されるはずだ。

map(stringDecoder, Number)(stringInput) => 数字

第2問

mapの型はなんだ?ヒント:型パラメーターが必要になる。

`map`の型
// O1とO2はそれぞれ元のデコーダと最終デコーダの出力の型(O -> Output)
const map = <O1, O2, I = O1>(decoder: Decoder<O1, I>, f: (t: O1) => O2): Decoder<O2, I> => (input: unknown) => ...

fは値を型O1から型O2へ変換する関数だ。もちろん、O1O2が必ずしも異なる型だとは限らない。例えば、fが数字を受け取り、その数字の倍を返す関数ならO1O2numberである。

mapにデコーダを渡すと、出力されるデコーダの出力が違う型を持つ可能性がある。ならば、最終デコーダの入力と出力の型も違う。

これこそ、幽霊型(デコーダ入力の型)の醍醐味だと考える。つまり、unknown型の入力から始め、どんな変換を経てもunknownの後ろに隠れていた入力の真型を記録することができる。また、この記録は完全に自動的に行われる。

map関数の本体

const map = <T, U, I = T>(decoder: Decoder<T, I>, f: (t: T) => U): Decoder<U, I> => 
	(input: unknown) => 
		Result.map(decoder(input), f);

decoderの返り値を求め、fを適用するだけ。しかし、問題はResult.mapだ。

Result.mapの型を見れば、その用途がわかる:

// Result.ts
const map = <T, U, Error>(result: Result<T, Error>, f: (t: T) => U): Result<U, Error> => ...

Result.mapResult内の値に関数を適用する。また、Resultはエラーであれば、そのエラーをそのまま返す。

第3問

ResultのサブタイプであるOkErrorは以下の通りならば、Result.map関数の本体はどうなる:

type Ok<T> = { tag: 'ok', value: T }
type Error<E> = { tag: 'error', error: E }
`Result.map`関数の本体
const map = <T, U, Error>(result: Result<T, Error>, f (t: T) => U): Result<U, Error> => {
	if (result.tag === 'ok')
	  return { tag: 'ok', value: f(result.value) }
	} 
	// エラーの場合、そのまま返す
	else {
	  return result
	}
}

mapの用例

最後に、mapデコーダの用例と入出力の型の例はこちら:

デコードしたオブジェクトを文字列に変換

const urlObjectDecoder = objectDecoder({
  scheme: stringDecoder,
  host: stringDecoder,
  path: stringDecoder
})

const urlObjectToStringDecoder = map(urlObjectDecoder, ({ scheme, host, path }) => `${scheme}://${host}/${path}`)

urlObjectToStringDecoderの入力の型:

type Input = {
  scheme: string
  host: string
  path: string
}

urlObjectToStringDecoderの出力の型:

type Output =  string
const urlString = urlObjectToStringDecoder({ scheme: 'https', host: 'www.example.com', path: 'path/to/something' })

urlStringの型はInput型と同値なので、Okが戻される:

urlString === { tag: 'ok', value: 'https://www.example.com/path/to/something' }

まとめ

今回は入出力の異なるデコーダに幽霊型を適用し、不定形なデータの変化を観察することができた。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?