Closed2

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

ShebangDogShebangDog

はじめに

今回はTypeScriptの型のみで四則演算を行う方法の紹介です。
読んだ方が確実に使えるようになるためにも詳細な部分まで説明します。
そのためこのトピックを何回かに分けて投稿していきます。

本記事を書いた目的

「自分が楽しいと思うことが共有したい」「その楽しいと思うことの敷居を下げる」ことです。ぜひこの記事で型周りのお話しへ入門してみてください!!

また投稿にあたって参考にした文献も載せるので「興味あるけどよくわからなかった」「もっと知りたい」と感じた方はぜひ参考文献の方も目を通してみてください。

最終的に作れるようになるもの

記事全てを読むことで型レベルで以下のような四則演算が可能になります。

Arithmetic.ts
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

それぞれ紹介していきます。
今回は以下のオブジェクトの型を宣言した場合の例示をします。

objectSample.ts
const user = {
  name: "ShebangDog",
  age: 5,
}

type

typeでは以下のように型の宣言を行います。

typeUsage.ts
type User = {
  name: string
  age: number
}

interface

interfaceでは以下のように型の宣言を行います。

interfaceUsage.ts
interface User {
  name: string
  age: number
}

型を関数として捉える

そもそも関数とは何かですが、IT用語辞典には以下のように説明されています。

関数とは、コンピュータプログラム上で定義されるサブルーチンの一種で、数学の関数のように与えられた値(引数)を元に何らかの計算や処理を行い、結果を呼び出し元に返すもののこと。
引用: 関数とは

関数というのは「値を受け取り値を返すもの」という認識になりがちですが、そうではなく「何かを受け取り何かを返す」という認識を持つこともできます。そのように考えることで型の世界にある関数が見えてきます。

その型の一つがArray<T>です。この型は引数を一つ受け取りそれを使って配列の型を返します。
Tを受け取ってArray<T>を返すのがArrayがしていることです。

ArrayType.ts
  type NumberArray = Array<number>

私たちのよく知る関数に置き換えるなら以下のような関数になるかと思います。

ArrayFunc.ts
  const createArray = (...args) => args
  const array = createArray(1, 2, 3, 4)

引数を受け取りそれを変換したものを結果として返す。
どうですか?型における関数の側面を感じられたでしょうか。

型の世界における引数

型の世界にも引数を受けとる手法が存在します。それがGenericsです。
出力可能なオブジェクトの型としてPrintableという型を作ってみます。TypeScriptでは以下のように記述します。

usageGenerics.ts
type Printable<T> = {
  print: (value: T) => void
  value: T
}

このPrintable<T>Tという形で任意の型を受け取り{ print: (value: T) => void, value: T }なオブジェクトの型を作ります。受け取った型は関数の引数と同様でその定義内で使うことができます、これがタイトルで「型の世界における引数」と言っている所以です。

型レベルの関数ではこれを利用して引数を受け取っています。

型の世界における分岐

型の世界にも分岐を担う機能が存在します。それがConditional typesです。
この機能は型が特定の型を含んでいるかどうかを検査し、それに応じて型を返す機能です。

これを使ってTを受け取りTstringであれば"isString"型をそうでなければ"isNotString"型を返す型を作ります。この型定義は以下のようになります。

Bifurcation.ts
type StringOrNot<T> = T extends string ? "isString" : "isNotString"

この型ではConditional typesが利用されていて条件式 ? 条件式が真の場合 : 条件式が偽の場合と書きます。見た目も動作も条件演算子と同じです。

例えばこれを利用して"left"型か"right"型を受け取り特定の型を返す型を作ることもできます。

GetPair.ts
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>を作ります。

inference.ts
type ReturnType<Func> = Func extends ((...param: any[]) => infer R) ? R : never

これを実装するには以下の順に考えると良いです。

  1. 受け取った型が関数の型だと仮定する
    2.返却値の型を取得する
  2. それを結果として返す

受け取った型が関数の型だと仮定する

これを実現するには先ほど紹介したextendsを使います

extends.ts
type ReturnType<Func> = 
  Func extends ((...param: any[]) => infer R) // ここで型を仮定する
   ? R
   : never

返却値の型を取得する

これを実現するにはinferを使います。

infer.ts
type ReturnType<Func> = 
  Func extends ((...param: any[]) => infer R) // ここで型を仮定しPとRを取得する
   ? R
   : never

inferを使うことで今回で言えば関数の構造をヒントにして型の推論が行われ、推論の結果がPやRに入ります。例えば、下のコードではPにnumberが入ります。

inferNumber.ts
type Result = ReturnType<(param: number) => string>

それを結果として返す

最後はinferで推論したRを返すことでこの型の実装を終えます。

returnType.ts
type ReturnType<Func> = 
  Func extends ((...param: any[]) => infer R)
   ? R // 推論した型を返す
   : never

inferを使えば配列の要素の型を取得することもできます。

終わり

本記事ではここまでとします。
次回の記事では「関数のような型を作ることに慣れる」「カウンターの実装する」を目標に書きます。
ちなみに今回作ったReturnTypeも関数のような型です。

part2: https://zenn.dev/shebangdog/scraps/e347a4f9261078

次回することのチラ見せ

次回の記事では次の型の実装が理解できるようになります。

counter.ts
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"]

参考文献

https://numb86-tech.hatenablog.com/entry/2020/07/02/103544

https://qiita.com/Quramy/items/b45711789605ef9f96de

https://typescriptbook.jp/reference/generics

ShebangDogShebangDog

型が特定の型を含んでいるかどうかを検査し

これはよく聞く言い方に変えると「型が互換性を持っているかを検査する」です。

このスクラップは2023/09/28にクローズされました