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

はじめに
タイトルにある通りPart2となります。Part1を読んだ前提で話を進めます。
Part1の記事はこちらになります。
また今回は特に手を動かして欲しいと考えています。
TSのPlayground上で動かしたり試したりするのにちょうど良いものを用意したので、ぜひご自身で書いてみてください。
この記事単体でわかるようになること
- 型コンストラクタの存在
- 型の世界における文字列操作
お知らせ
前回
次回の記事では「関数のような型を作ることに慣れる」「カウンターの実装する」を目標に書きます。
と言ったのですが「カウンターを実装する」の部分は次回に回します。
前回の振り返り
前回は2つのことがわかりました。
extends
について
型宣言で見る型を判別するためにextends
を利用していて、それをConditional Types
と言います。
条件演算子と同じ使い方ができるものでした。
type LorR = "left" | "right"
type GetPair<A extends LorR, L, R> = A extends "left" ? L
: A extends "right"
? R
: never
infer
について
たまに見る型を推論し、名前をつける為にinfer
というものがありました。
Conditonal types
と組み合わせて使うことで型の推論, 取得をおこなっていました。
type ReturnType<Func> = Func extends ((...param: any[]) => infer R) ? R : never
今回はこれらの機能を型コンストラクタ作成の道具として用います。
型コンストラクタとは
型コンストラクタとは型を引数として受け取り型を返す型のことを言います。
また前回作ったReturnType
はFunc
を受け取り、Func
の返却値の型を返す型コンストラクタです。
type ReturnType<Func> = Func extends ((...param: any[]) => infer R) ? R : never
他にも身近に型コンストラクタはあってUtilityType
なんかがそうです。
実際に作ってみましょう
手始めにいくつか型コンストラクタを作ってみましょう
ElementOf
名前: ElementOf
引数: 1つ受け取る。制約はない(制約ありでも作れるのでお任せします)
機能: 受け取った型がArray
であればその要素の型を結果として返す
// `ElementOf`の定義
type Assert<L, R extends L> = L extends R ? true : false
type Result = Assert<ElementOf<string[]>, string>
実装例
type ElementOf<Lst extends unknown[]> = Lst extends Array<infer E> ? E : never
First
課題としてはElementOf
の他にFirst
とか作ってみると面白いかもしれません。
タプルの頭の型を取る型です。
type Result = Assert<First<[1, 2, 3]>, 1>
実装例
type First<Lst extends unknown[]> = Lst extends [infer Head, ...unknown[]] ? Head : never
型の世界における文字列操作
なんと型の世界にも文字列が存在します。これはstring
型のことではなく、特定の文字列を限定する文字列型を指します。例えば"hello"
型や"world"
型です。
Literal Types
TypeScriptにはLiteral Types
というプリミティブ型の特定の値を表現できる型があります。
string
型はそのプリミティブな型のうちの一つです。そのため"hello"
型や"world"
型などのstring
型の特定の値を型として定義できます。
type Hello = "hello"
type World = "world"
もちろんこれは型なので"hello"
型の値に対してnumber
型にあたる値を利用するとコンパイルエラーとなります。string
も同様で"hello"以外の値は受け付けません。
type Hello = "hello"
const hello: Hello = "hello" // ok
const one: Hello = 1 // Type '1' is not assignable to type '"hello"'
const string: Hello = "string" // Type '"string"' is not assignable to type '"hello"'
このような型があると先ほどの"hello"
型と"world"
型を組み合わせて"hello world"
型を作りたくなると思います。それを叶えるのがTemplate Literal Types
です。
Template Literal Types
TypeScriptには特定の型やその組み合わせを含んだ文字列型としてTemplate Literal Types
があります。言葉での説明が難しいのでコードに頼りながら紹介していきます。
基本のTemplate Literal Types
Template Literal Types
の書き方はJavaScriptにあるTemplate String
と同じです。
Template String
とはバッククォートで文字列を括ると、その中で変数や値を文字列として展開できる文字列のことです。
const hello = "hello"
const helloWorld = `${hello} world`
console.log(helloWorld) // hello world
先ほど言ったようにTemplate Literal Types
もバッククォートでプリミティブな型を括ります。
type Hello = "hello"
type HelloWorld = `${Hello} world`
type Result = Assert(HelloWorld, "hello world")
Template Literal Types
特定の型を含んだtype Hello = "hello"
type Name = string
type HelloShebang = `${Hello} ${Name}`
const helloShebang: HelloShebang = "hello shebang" // ok
const goodbyeShebang: HelloShebang = "goodbye shebang" // Type '"goodbye shebang"' is not assignable to type '`hello ${string}`'
Template Literal Types
特定の型の組み合わせを含んだTemplate Literal Types
のいいところはUnion Type
を含めることです。
これができることで表現力の高い型が定義できます。
type Gopher = "gopher"
type Monalisa = "monalisa"
type Someone = Gopher | Monalisa
type Greeting = `hello ${Someone}`
const helloGopher: Greeting = "hello gopher" // ok
const helloMonalisa: Greeting = "hello monalisa" // ok
const helloSomeone: Greeting = "hello someone" // Type '"hello someone"' is not assignable to type '"hello gopher" | "hello monalisa"'.
またこのTemplate Literal Types
を使えば特定の型を連結させることもできます。
type Hello = "hello"
type World = "world"
type HelloWorld = `${Hello} ${World}`
const helloWorld: HelloWorld = "hello world" // ok
型コンストラクタを作ってみましょう
Concat
名前: Concat
引数: 2つ受け取り、2つともstring
を含む型であること
(e.g type Concat<L extends string, R extends string> = //
)
機能: 受け取った型を連結させ、文字列型としたものを結果として返す
type Result = Assert<Concat<"concat", "ing">, "concating">
実装例
type Concat<L extends string, R extends string> = `${L}${R}`
SplitOnce
名前: SplitOnce
引数: 2つ受け取り、それぞれstring
を含む型であること
機能: 2つめの型を区切り文字とし、1つ目の型を二つの文字列(タプル)に分けて返す
type Result = Assert<SplitOnce<"hello world", " ">, ["hello", "world"]>
実装例
type SplitOnce<Str extends string, Delimiter extends string> = Str extends `${infer Head}${Delimiter}${infer Tail}`
? [Head, Tail]
: Str
Split
名前: Split
引数: 2つ受け取り、それぞれstring
を含む型であること
機能: 2つめの型を区切り文字とし、1つ目の型を分けてタプルを返す
type Result = Assert<
Split<"you should implement Recursive functions", " ">,
["you", "should", "implement", "Recursive", "functions"]
>
実装例
type Split<Str extends String, Delimiter extends string, Result extends string[] = []> = Str extends `${infer Head}${Delimiter}${infer Tail}`
? Split<Tail, Delimiter, [...Result, Head]>
: [...Result, Str]
型の世界における再帰
型コンストラクタは引数を受け取り、結果を返すことのできる型です。型の世界に繰り返し(for
)は存在しませんが、分岐(Conditional Types
)は存在するので引数を受け取ることはできます。そのため再帰関数的動作を定義できます。
ではどのようなときに再帰関数的動作が必要になるのでしょうか。
今回は文字列を連結させる型コンストラクタConcat
を作りましたが、3つの文字列を連結させるとなると少し不便です。
type Result = Concat<"first-", Concat<"second-", "third">>
こういった場合に再帰関数的動作を利用すれば以下のような形で3つの文字列を連結させることができるでしょう。
type Result = ConcatRecursive<["first-", "second-", "third"]>
実際に再帰を利用して定義したConcatRecursive
を紹介します。
type ConcatRecursive<Lst extends string[], Result extends string = ""> = Lst extends [infer Head, ...infer Tail]
? Tail extends string[] ? Head extends string ? ConcatRecursive<Tail, `${Result}${Head}`> : never : never
: Result
1. 引数の準備
今回作る文字列連結関数は大雑把にいうとstring[]
を受け取りstring
にする関数です。
またこの関数は再帰関数とします。
引数は主に二つ受け取ると良いでしょう。
1つは連結する文字列のリストでもう一つは再帰の計算結果を記憶するための文字列です。
型はstring[]
とstring
です。
また計算結果をもつ為に用意した引数の初期値は""
とすると便利です。
type ConcatRecursive<Lst extends string[], Result extends string = ""> = // ...
2. 順番に処理する
再帰関数を作るわけなので配列の要素一つを順番に処理していくのが良いでしょう。
infer
を使って配列の先頭の要素と残りの要素とに分けます。
type ConcatRecursive<Lst extends string[], Result extends string = ""> =
+ Lst extends [infer Head, ...infer Tail] // ...
次にinfer
で取り出したHead
を計算に用いて、Tail
を次の配列とします。
こうすることで配列一つずつを処理できます。
ここまで理解すれば受け取った配列を反転させるReverse
を作れるかと思います。
type Reverse<E, Lst extends E[], Result extends E[] = []> = Lst extends [infer Head, ...infer Tail]
+ ? Tail extends E[]
+ ? Head extends E
? Reverse<E, Tail, [Head, ...Result]>
+ : never
+ : never
: Result
ここで注意なのですが引数に制約がある場合infer
で取得した型が制約通りかを調べてから使う必要があります。そのためHead extends E ?
やTail extends E[] ?
が必要です。また制約通りではない場合はnever
を返します。(ここでnever
を返すのはTail
がE[]
でないことやHead
がE
でないことがないからです。)
これでなんとなく再帰の扱い方がわかったのではないでしょうか。
3. 文字列を連結させる
最後に文字列の連結のさせ方です。
これにはTemplate Literal Types
を利用します。
type ConcatRecursive<Lst extends string[], Result extends string = ""> = Lst extends [infer Head, ...infer Tail]
? Head extends string
? Tail extends string[]
+ ? ConcatRecursive<Tail, `${Result}${Head}`>
: never
: never
: Result
${Result}${Head}
とすることで前回の連結結果(Result
)と今回連結する文字列(Head
)の連結ができます。
終わり
今回は型コンストラクタをたくさん紹介、実装しました。
そろそろextends
やinfer
も見慣れてきた頃ではないでしょうか。
また今回は再帰関数を利用することで繰り返しの処理も行えることを紹介しました。
これを利用することで本記事では取り上げていないいろんな型を作れるので、ぜひ作ってみたい型を探して作ってみてください。
次回「カウンターの実装する」を目標に書きます。
なんとTSの配列を駆使すればカウンターが作れるんです。次回を楽しみにしていてください。
次回することのチラ見せ
次回はカウンターの実装です!
なんと!TSの配列とConditional types
とinfer
を使うと配列の要素数が取れてしまいます!!!
配列の中の要素が3つであれば3
を、4つであれば4
が取れます。number
ではないんです。
これはつまり配列の要素数を変えることが値の増減へ間接的につながっちゃうってことです。
要素数を1増やす = カウントアップするです。
interface ICounter<Lst extends 0[] = []> {
count: Lst extends { length: infer C } ? C : never
up: ICounter<[...Lst, 0]>
}
参考文献