TypeScriptでの型を使った四則演算Part1

はじめに
今回はTypeScriptの型のみで四則演算を行う方法の紹介です。
読んだ方が確実に使えるようになるためにも詳細な部分まで説明します。
そのためこのトピックを何回かに分けて投稿していきます。
本記事を書いた目的
「自分が楽しいと思うことが共有したい」「その楽しいと思うことの敷居を下げる」ことです。ぜひこの記事で型周りのお話しへ入門してみてください!!
また投稿にあたって参考にした文献も載せるので「興味あるけどよくわからなかった」「もっと知りたい」と感じた方はぜひ参考文献の方も目を通してみてください。
最終的に作れるようになるもの
記事全てを読むことで型レベルで以下のような四則演算が可能になります。
type PlusedTwoAndTwelve = Plus<2, 12> // 加算用の型の利用例
type MinusedTwelveWithFour = Minus<12, 4> // 減算用の型の利用例
type MultipliedTwoAndTwo = Multiply<2, 2> // 乗算用の型の利用例
type DividedTenWithTwo = Divide<10, 2> // 徐算用の型の利用例
最終的に分かるようになること
- 型のみでの四則演算の表現の仕方
- 型の世界でのカウンターの作り方
- 型コンストラクタの存在
- たまにみる
infer
について - 型宣言で見る
extends
について - 型の世界の楽しさについて
- 僕が最近までしていたこと
この記事単体で分かるようになること
- たまにみる
infer
について - 型宣言で見る
extends
について
対象読者
- 型のある言語に触れたことがある
- Genericsに触れたことがある
- lambda式, アロー関数に触れたことがある
- 再帰関数の存在を知っている
まずは型の宣言に触れる
型宣言の仕方には以下の2種類があります。
- type
- interface
それぞれ紹介していきます。
今回は以下のオブジェクトの型を宣言した場合の例示をします。
const user = {
name: "ShebangDog",
age: 5,
}
type
type
では以下のように型の宣言を行います。
type User = {
name: string
age: number
}
interface
interface
では以下のように型の宣言を行います。
interface User {
name: string
age: number
}
型を関数として捉える
そもそも関数とは何かですが、IT用語辞典には以下のように説明されています。
関数とは、コンピュータプログラム上で定義されるサブルーチンの一種で、数学の関数のように与えられた値(引数)を元に何らかの計算や処理を行い、結果を呼び出し元に返すもののこと。
引用: 関数とは
関数というのは「値を受け取り値を返すもの」という認識になりがちですが、そうではなく「何かを受け取り何かを返す」という認識を持つこともできます。そのように考えることで型の世界にある関数が見えてきます。
その型の一つがArray<T>
です。この型は引数を一つ受け取りそれを使って配列の型を返します。
T
を受け取ってArray<T>
を返すのがArray
がしていることです。
type NumberArray = Array<number>
私たちのよく知る関数に置き換えるなら以下のような関数になるかと思います。
const createArray = (...args) => args
const array = createArray(1, 2, 3, 4)
引数を受け取りそれを変換したものを結果として返す。
どうですか?型における関数の側面を感じられたでしょうか。
型の世界における引数
型の世界にも引数を受けとる手法が存在します。それがGenerics
です。
出力可能なオブジェクトの型としてPrintable
という型を作ってみます。TypeScript
では以下のように記述します。
type Printable<T> = {
print: (value: T) => void
value: T
}
このPrintable<T>
はT
という形で任意の型を受け取り{ print: (value: T) => void, value: T }
なオブジェクトの型を作ります。受け取った型は関数の引数と同様でその定義内で使うことができます、これがタイトルで「型の世界における引数」と言っている所以です。
型レベルの関数ではこれを利用して引数を受け取っています。
型の世界における分岐
型の世界にも分岐を担う機能が存在します。それがConditional types
です。
この機能は型が特定の型を含んでいるかどうかを検査し、それに応じて型を返す機能です。
これを使ってT
を受け取りT
がstring
であれば"isString"
型をそうでなければ"isNotString"
型を返す型を作ります。この型定義は以下のようになります。
type StringOrNot<T> = T extends string ? "isString" : "isNotString"
この型ではConditional types
が利用されていて条件式 ? 条件式が真の場合 : 条件式が偽の場合
と書きます。見た目も動作も条件演算子と同じです。
例えばこれを利用して"left"
型か"right"
型を受け取り特定の型を返す型を作ることもできます。
type LorR = "left" | "right"
type GetPair<A extends LorR, L, R> = A extends "left" ? L
: A extends "right"
? R
: never
型の世界における推論
型の世界にも推論が存在します。TypeScriptではinfer
により推論を行います。
これはextends
と組み合わせて使うことで好きな型を取得できる機能です。
説明のために関数の返却値の型を取得するReturnType<Func>
を作ります。
type ReturnType<Func> = Func extends ((...param: any[]) => infer R) ? R : never
これを実装するには以下の順に考えると良いです。
- 受け取った型が関数の型だと仮定する
2.返却値の型を取得する - それを結果として返す
受け取った型が関数の型だと仮定する
これを実現するには先ほど紹介したextends
を使います
type ReturnType<Func> =
Func extends ((...param: any[]) => infer R) // ここで型を仮定する
? R
: never
返却値の型を取得する
これを実現するにはinfer
を使います。
type ReturnType<Func> =
Func extends ((...param: any[]) => infer R) // ここで型を仮定しPとRを取得する
? R
: never
infer
を使うことで今回で言えば関数の構造をヒントにして型の推論が行われ、推論の結果がPやRに入ります。例えば、下のコードではPにnumber
が入ります。
type Result = ReturnType<(param: number) => string>
それを結果として返す
最後はinfer
で推論したR
を返すことでこの型の実装を終えます。
type ReturnType<Func> =
Func extends ((...param: any[]) => infer R)
? R // 推論した型を返す
: never
infer
を使えば配列の要素の型を取得することもできます。
終わり
本記事ではここまでとします。
次回の記事では「関数のような型を作ることに慣れる」「カウンターの実装する」を目標に書きます。
ちなみに今回作ったReturnType
も関数のような型です。
part2: https://zenn.dev/shebangdog/scraps/e347a4f9261078
次回することのチラ見せ
次回の記事では次の型の実装が理解できるようになります。
type FixedArray<E, L extends number, Result extends E[] = []> = Result["length"] extends L
? Result
: FixedArray<E, L, [...Result, E]>
type CounterElement = unknown
type Counter<Value extends number> = {
count: Value
up: Counter<Up<Value>>
down: Counter<Down<Value>>
}
type Up<Value extends number> = ([...FixedArray<CounterElement, Value>, CounterElement] extends [...infer U] ? U : [])["length"]
type Down<Value extends number> = (FixedArray<CounterElement, Value> extends [CounterElement, ...infer Rest] ? Rest : [])["length"]
参考文献

型が特定の型を含んでいるかどうかを検査し
これはよく聞く言い方に変えると「型が互換性を持っているかを検査する」です。