Closed1

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

ShebangDogShebangDog

はじめに

タイトルにある通りPart2となります。Part1を読んだ前提で話を進めます。
Part1の記事はこちらになります。

https://zenn.dev/link/comments/2f0d0e5176687f

また今回は特に手を動かして欲しいと考えています。
TSのPlayground上で動かしたり試したりするのにちょうど良いものを用意したので、ぜひご自身で書いてみてください。
https://www.typescriptlang.org/

この記事単体でわかるようになること

  • 型コンストラクタの存在
  • 型の世界における文字列操作

お知らせ

前回

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

と言ったのですが「カウンターを実装する」の部分は次回に回します。

前回の振り返り

前回は2つのことがわかりました。

型宣言で見るextendsについて

型を判別するためにextendsを利用していて、それをConditional Typesと言います。
条件演算子と同じ使い方ができるものでした。

getPair.ts
type LorR = "left" | "right"
type GetPair<A extends LorR, L, R> = A extends "left" ? L 
  : A extends "right" 
    ? R 
    : never

たまに見るinferについて

型を推論し、名前をつける為にinferというものがありました。
Conditonal typesと組み合わせて使うことで型の推論, 取得をおこなっていました。

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

今回はこれらの機能を型コンストラクタ作成の道具として用います。

型コンストラクタとは

型コンストラクタとは型を引数として受け取り型を返す型のことを言います。

また前回作ったReturnTypeFuncを受け取り、Funcの返却値の型を返す型コンストラクタです。

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

他にも身近に型コンストラクタはあってUtilityTypeなんかがそうです。
https://www.typescriptlang.org/docs/handbook/utility-types.html#example

実際に作ってみましょう

手始めにいくつか型コンストラクタを作ってみましょう

https://www.typescriptlang.org/

ElementOf

名前: ElementOf
引数: 1つ受け取る。制約はない(制約ありでも作れるのでお任せします)
機能: 受け取った型がArrayであればその要素の型を結果として返す

CheckElementOf.ts
// `ElementOf`の定義
type Assert<L, R extends L> = L extends R ? true : false
type Result = Assert<ElementOf<string[]>, string>
実装例
ElementOf.ts
type ElementOf<Lst extends unknown[]> = Lst extends Array<infer E> ? E : never

https://www.typescriptlang.org/play?#code/C4TwDgpgBAggznCAnYAeAMgGigJShAD2AgDsATOKdAPigF4r8jSLcoB+KYJAV2gC4oAMwCGAG0QAoSaEhQAomIgBbUsADyQjHGBNi5SjxIBrEgHsA7iQDaAXVoN0OvS0owkSESFQBLEkOQFWk55KEESCAA3ZGlZaBwIOB4xXQZ4RBRURRU1TVQdJD8AcztqbALi6iA

First

課題としてはElementOfの他にFirstとか作ってみると面白いかもしれません。
タプルの頭の型を取る型です。

CheckFirst.ts
type Result = Assert<First<[1, 2, 3]>, 1>
実装例
First.ts
type First<Lst extends unknown[]> = Lst extends [infer Head, ...unknown[]] ? Head : never

https://www.typescriptlang.org/play?#code/C4TwDgpgBAggznCAnYAeAMgGigJShAD2AgDsATOKdAPigF4r8jSLcoB+KYJAV2gC4oAMwCGAG0QAoSaEhQAYgEskcNOlVNi5SjxIBrEgHsA7iQDaAXVoN1wTS0pnFJIcigAJCCLLYAdP90DE3MLCw4PLzIoQRIIADdkaVloHAg4HjE7BnhEFFQlFTQzAEZsACZsAGYrbGLqIA

型の世界における文字列操作

なんと型の世界にも文字列が存在します。これはstring型のことではなく、特定の文字列を限定する文字列型を指します。例えば"hello"型や"world"型です。

Literal Types

TypeScriptにはLiteral Typesというプリミティブ型の特定の値を表現できる型があります。
string型はそのプリミティブな型のうちの一つです。そのため"hello"型や"world"型などのstring型の特定の値を型として定義できます。

literalTypes.ts
type Hello = "hello"
type World = "world"

もちろんこれは型なので"hello"型の値に対してnumber型にあたる値を利用するとコンパイルエラーとなります。stringも同様で"hello"以外の値は受け付けません。

usageLiteralTypes.ts
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とはバッククォートで文字列を括ると、その中で変数や値を文字列として展開できる文字列のことです。

templateString.js
const hello = "hello"
const helloWorld = `${hello} world`

console.log(helloWorld) // hello world

先ほど言ったようにTemplate Literal Typesもバッククォートでプリミティブな型を括ります。

templateLiteralTypes.ts
type Hello = "hello"
type HelloWorld = `${Hello} world`

type Result = Assert(HelloWorld, "hello world")

特定の型を含んだTemplate Literal Types

includeSpecifiedType.ts
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を含めることです。
これができることで表現力の高い型が定義できます。

includeComposedType.ts
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を使えば特定の型を連結させることもできます。

helloWorld.ts
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> = //)
機能: 受け取った型を連結させ、文字列型としたものを結果として返す

CheckConcat ts
type Result = Assert<Concat<"concat", "ing">, "concating">
実装例
Concat.ts
type Concat<L extends string, R extends string> = `${L}${R}`

https://www.typescriptlang.org/play?#code/C4TwDgpgBAggznCAnYAeAMgGigJShAD2AgDsATOKdAPigF4r8jSLcoB+KYJAV2gC4oAMwCGAG0QAoSaEhQAwgHsSAYxFp0TYuUpxuASxIBzbHkLbWepIaO0GAAwAkAb3QBfFzjf3ps6Dgg4HjFgelgEZDQlVXVUACIVZTVgOOw4mzjqNMSY4AzqIA

SplitOnce

名前: SplitOnce
引数: 2つ受け取り、それぞれstringを含む型であること
機能: 2つめの型を区切り文字とし、1つ目の型を二つの文字列(タプル)に分けて返す

CheckSplitOnce ts
type Result = Assert<SplitOnce<"hello world", " ">, ["hello", "world"]>
実装例
SplitOnce.ts
type SplitOnce<Str extends string, Delimiter extends string> = Str extends `${infer Head}${Delimiter}${infer Tail}` 
  ? [Head, Tail]
  : Str

https://www.typescriptlang.org/play?#code/C4TwDgpgBAggznCAnYAeAMgGigJShAD2AgDsATOKdAPigF4r8jSLcoB+KYJAV2gC4oAMwCGAG0QAoSaEhQAymDEBLYAHkSAYwip53JsXKU43ZSQDm2ACIQVAW1XIDLY6Yu0GepM6NQABgAkAN5mQk4AEhAiZAC+wTb2jkhxISRh3gAqIspiMX5QklAcUADakdHYWTkAuoVQgl7SstA4EHA8YsD0sAjIaIoq6lo6AEQAFrZiAPZQAO5TSGJkI9gjUCPU2CXjk1Mr6-OLy9XUkkA

Split

名前: Split
引数: 2つ受け取り、それぞれstringを含む型であること
機能: 2つめの型を区切り文字とし、1つ目の型を分けてタプルを返す

CheckSplit.ts
type Result = Assert<
  Split<"you should implement Recursive functions", " ">,
  ["you", "should", "implement", "Recursive", "functions"]
>
実装例
Split.ts
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]

https://www.typescriptlang.org/play?#code/C4TwDgpgBAggznCAnYAeAMgGigJShAD2AgDsATOKdAPigF4r8jSLcoB+KYJAV2gC4oAMwCGAG0QAoSaEhQAymDEBLNPO5Ni5SuqTKSAc2wARCCoC2q5JpaU43fUdwQ4PMcBvao9vYYDaALr0UIG0DLqerAAGACQA3vpC1gASECJkAL7xphZWSFkJJElIUAAqIspiGVFQklAcCkqqqOWVJmbKlsRI2H4AdAM4Lm7A2KnpAdR1UIL9g8Pu2LoB0rLQQ67uwfCIKKjTiipoAEQgAPY83gAWF2JkUJ1KEOakHkMAxjxIcMoAbtBCHgkd7AZRnEhwY7YY5QY7UTDTPynC5Q2FwG5uMio46PMTPV7Yj5fH7-bGA4Gg8GQlZTGTgaAAUXMYFA2wQyDQByaJ2xsPhiOOxxpkiAA

型の世界における再帰

型コンストラクタは引数を受け取り、結果を返すことのできる型です。型の世界に繰り返し(for)は存在しませんが、分岐(Conditional Types)は存在するので引数を受け取ることはできます。そのため再帰関数的動作を定義できます。

ではどのようなときに再帰関数的動作が必要になるのでしょうか。
今回は文字列を連結させる型コンストラクタConcatを作りましたが、3つの文字列を連結させるとなると少し不便です。

threeConcat.ts
type Result = Concat<"first-", Concat<"second-", "third">>

こういった場合に再帰関数的動作を利用すれば以下のような形で3つの文字列を連結させることができるでしょう。

threeConcatRecursive.ts
type Result = ConcatRecursive<["first-", "second-", "third"]>

実際に再帰を利用して定義したConcatRecursiveを紹介します。

concatRecursive.ts
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です。
また計算結果をもつ為に用意した引数の初期値は""とすると便利です。

concatRecursiveStep1.ts
type ConcatRecursive<Lst extends string[], Result extends string = ""> = // ...

2. 順番に処理する

再帰関数を作るわけなので配列の要素一つを順番に処理していくのが良いでしょう。
inferを使って配列の先頭の要素残りの要素とに分けます。

concatRecursiveStep2.ts
type ConcatRecursive<Lst extends string[], Result extends string = ""> = 
+ Lst extends [infer Head, ...infer Tail] // ...

次にinferで取り出したHeadを計算に用いて、Tailを次の配列とします。
こうすることで配列一つずつを処理できます。
ここまで理解すれば受け取った配列を反転させるReverseを作れるかと思います。

reverse.ts
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を返すのはTailE[]でないことやHeadEでないことがないからです。)

これでなんとなく再帰の扱い方がわかったのではないでしょうか。

3. 文字列を連結させる

最後に文字列の連結のさせ方です。
これにはTemplate Literal Typesを利用します。

concatRecursiveStep3.ts
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)の連結ができます。

https://www.typescriptlang.org/play?#code/C4TwDgpgBAggznCAnYAeAMgGigJShAD2AgDsATOKdAPigF4r8jSLcoB+KYJAV2gC4oAMwCGAG0QAoSaEhQAwgHsSAYxHAcEFTyRwAlgDcIGOMCbFylU0j0kA5gG0Autk1weYs4Qutrtu-RQAERBtAzopuYslA62QshQABIQImTYAHSZcQkAKiJ6Yk5QklAcSSlkUZZQfvbFpaWceQVVvtz+ziUNnEqq6prauobGzWLYAAYAJADebh7AAL4zyakL49RdDVCCJBBGSJvbULv7XYJzntKy0BdmDPCIKKi9ahpaOvpGqA5BQnq6wCC2CCAFogcFECplGRwaDYcAABb-GFOajAv4AkGQ6EgxHI0JAA

終わり

今回は型コンストラクタをたくさん紹介、実装しました。
そろそろextendsinferも見慣れてきた頃ではないでしょうか。
また今回は再帰関数を利用することで繰り返しの処理も行えることを紹介しました。
これを利用することで本記事では取り上げていないいろんな型を作れるので、ぜひ作ってみたい型を探して作ってみてください。

次回「カウンターの実装する」を目標に書きます。
なんとTSの配列を駆使すればカウンターが作れるんです。次回を楽しみにしていてください。

次回することのチラ見せ

次回はカウンターの実装です!
なんと!TSの配列とConditional typesinferを使うと配列の要素数が取れてしまいます!!!
配列の中の要素が3つであれば3を、4つであれば4が取れます。numberではないんです。
これはつまり配列の要素数を変えることが値の増減へ間接的につながっちゃうってことです。
要素数を1増やす = カウントアップするです。

iCounter.ts
interface ICounter<Lst extends 0[] = []> {
  count: Lst extends { length: infer C } ? C : never
  up: ICounter<[...Lst, 0]>
}

参考文献

https://qiita.com/rooooomania/items/e9d29c27abfe2aa9d8c6
https://qiita.com/poly_soft/items/1d7363b85b04e48a6e24
https://blog.rockthejvm.com/scala-types-kinds/
https://typescriptbook.jp/reference/values-types-variables/literal-types
https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html
https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types
https://typescriptbook.jp/reference/values-types-variables/union
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
https://aroundthedistance.hatenadiary.jp/entry/2015/06/17/182259

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