Slide 1

Slide 1 text

TypeScript で型レベル JSON パーサ Think! FrontEnd #8 合同会社 DMM.com 電子書籍開発部 25 新卒 加藤 豪

Slide 2

Slide 2 text

自己紹介 本名 加藤 豪 ニックネーム ERASER 大学 会津大学 領域 Web FE 趣味 ゲーム、イラスト見る、VTuber 所属 二次元開発本部 電子書籍開発部 1 / 36

Slide 3

Slide 3 text

本日のお品書き 1. はじめに • 今回作ったもの • デモ 2. 概要 • JSON パーサの構成 • Tokenizer とは • Parser とは 3. 実装 • Tokenizer の実装 • Parser の実装 • ところどころで実装テクニックを解説 4. 終わりに • やりきれなかったこと • 感想 2 / 36

Slide 4

Slide 4 text

はじめに

Slide 5

Slide 5 text

今回作ったもの 型レベル JSON パーサ 文字列リテラル型を JSON として解析する 型 type Input = '{ "key": 123 }'; type Result = JsonParser; type Expected = { key: 123 }; 4 / 36

Slide 6

Slide 6 text

型…??? 🤔 様子がおかしいな…

Slide 7

Slide 7 text

はじめに ❌ JSON を解析する実行プログラム ⭕ JSON を解析する型 5 / 36

Slide 8

Slide 8 text

期待している方にお断り 1. 型なのでコンパイル時に JSON がわかってないと使えない • 動的に JSON を解析することはできない • 例えば API のレスポンスを解析することはできない 2. JSON の仕様に準拠しているわけではない • 適当です 6 / 36

Slide 9

Slide 9 text

概要

Slide 10

Slide 10 text

JSON パーサの構成 作成した JSON パーサの構成は大きく 2 つに分かれる • Tokenizer 字句解析 入力文字列をトークンに分割 • Parser 構文解析 トークン列型を解析して最終的な型に変換する 8 / 36

Slide 11

Slide 11 text

Tokenizer とは 文字列をトークンに分割する トークン 意味のある最小単位の文字列を表すもの • 例: ‣ 1234 -> NumberToken<1234> ‣ "hello" -> StringToken<"hello"> ‣ { -> LeftBraceToken 9 / 36

Slide 12

Slide 12 text

Tokenizer とは 文字列をトークンに分割する '{ "key": 123 }' ↓ 分割 • { LeftBraceToken • “key” StringToken<"key"> • : ColonToken • 123 NumberToken<123> • } RightBraceToken 10 / 36

Slide 13

Slide 13 text

Parser とは トークン列型を解析して 最終的な型に変換する • { LeftBraceToken • “key” StringToken<"key"> • : ColonToken • 123 NumberToken<123> • } RightBraceToken ↓ 解析 { key: 123 } 11 / 36

Slide 14

Slide 14 text

実装

Slide 15

Slide 15 text

Token の種類 トークンを以下のように定義 export type Token = | SimpleToken | StringToken | NumberToken; export type StringToken<...> = ... export type NumberToken<...> = ... enum SimpleToken { LeftBrace, // { RightBrace, // } LeftBracket, // [ RightBracket, // ] Colon, // : Comma, // , True, // true False, // false Null, // null End, // 入力の終端 Bad // トークナイズに失敗 } 13 / 36

Slide 16

Slide 16 text

Tokenizer の実装(1/4) … 小さな Tokenizer まずは個別のトークンを抽出する 小さな Tokenizer を実装 これらはトークンを発見し以下を返す • 抽出したトークン • 消費文字数 14 / 36

Slide 17

Slide 17 text

Tokenizer の実装(1/4) … 小さな Tokenizer 例えば true を抽出する TrueTokenizer は以下のように実装 type TrueTokenizer = S extends `true${string}` ? [TrueToken, 4] : never; 15 / 36

Slide 18

Slide 18 text

ちょっと解説 1 TS の型演算には if 文が存在しない extends を使うと条件分岐ができる 型1 extends 型2 ? 分岐1 : 分岐2 16 / 36

Slide 19

Slide 19 text

ちょっと解説 2 以下のようなものを template literal types と言います `true${string}` これは true で始まる任意の文字列型にマッチします 17 / 36

Slide 20

Slide 20 text

Tokenizer の実装(2/4) 小さな Tokenizer を組み合わせて トークンを 1 つ抽出する TokenizeOnce を実装 type TokenizeOnce = | RightBraceTokenizer | LeftBraceTokenizer | ColonTokenizer | TrueTokenizer | FalseTokenizer | WhiteSpaceTokenizer | NullTokenizer | LeftSquareBracketTokenizer | RightSquareBracketTokenizer | StringTokenizer | CommaTokenizer | NumberTokenizer 18 / 36

Slide 21

Slide 21 text

Tokenizer の実装(3/4) … ループ処理 TokenizeOnce を使い文字列をループ処理して 文字列全体をトークン列に変換する TokenizerInner を実装 実装は複雑なので割愛します コードは載せておくので興味があれば見てください 19 / 36

Slide 22

Slide 22 text

Tokenizer の実装(3/4) … ループ処理 実際のコード type TokenizerInner< S extends string, AccTokens extends Token[] = [], End = EndTokenizer extends never ? false : true, Result extends [Token, number] = TokenizeOnce, NextToken extends Token = Result[0], ReadedLength extends number = Result[1] > = End extends true ? [...AccTokens, SimpleToken.End] : Result extends never ? [...AccTokens, SimpleToken.Bad] : NextToken extends SimpleToken.Bad ? [...AccTokens, SimpleToken.Bad] : TokenizerInner, [...AccTokens, NextToken]>; 20 / 36

Slide 23

Slide 23 text

ちょっと解説 3 TS の型演算には for や while が存在しない ただし再帰関数よろしく型を再起的に呼び出せる type MakeTupleN< T extends unknown, N extends number, Acc extends unknown[] = [] > = Acc['length'] extends N ? Acc : MakeTupleN; // 再帰 Listing 1: 長さ N のタプル型を作成 21 / 36

Slide 24

Slide 24 text

Tokenizer の実装(4/4) … 完成 型引数を隠蔽して完成! ! ! ! export type Tokenizer = TokenizerInner; 22 / 36

Slide 25

Slide 25 text

ふぅ…

Slide 26

Slide 26 text

Parser の実装(1/5) 特定の値を読み取る小さなパーサを実装 type ParseStringValue = Tokens extends [StringToken, ...infer Remain] ? [S, Remain] : never 23 / 36

Slide 27

Slide 27 text

Parser の実装(2/5) 小さなパーサを組み合わせてプリミティブな値(配列,オブジェクト以外) を読み取る ParsePrimitiveValue を実装 export type ParsePrimitiveValue = | ParseStringValue | ParseNumberValue | ParseBooleanValue | ParseNullValue | EncounteredBad 24 / 36

Slide 28

Slide 28 text

Parser の実装(3/5) オブジェクトをパースする ParseObject の実装 本実装は長いので割愛 export type ParseObject = Tokens extends [SimpleToken.LeftBrace, SimpleToken.RightBrace, ...infer RemainTokens extends Token[]] ? [{}, RemainTokens] : Tokens extends [SimpleToken.LeftBrace, ...infer RemainTokens extends Token[]] ? ParseFields : never 25 / 36

Slide 29

Slide 29 text

Parser の実装(4/5) 配列をパースする ParseArray の実装 本実装は例によって割愛 export type ParseArray = Tokens extends [SimpleToken.LeftSquareBracket, ...infer RemainTokens extends Token[]] ? ParseArrayElements : never 26 / 36

Slide 30

Slide 30 text

Parser の実装(5/5) ここまで作ったものを組み合わせて Parser の完成 export type Parser = | ParseObject | ParseArray | ParsePrimitiveValue; 27 / 36

Slide 31

Slide 31 text

そして

Slide 32

Slide 32 text

そして Json パーサの完成 出来上がった Tokenizer と Parser を組み合わせて完成! ! ! export type JsonParser = Parser< ArrayRemoveElement, SimpleToken.WhiteSpace> > extends [infer Result, ...infer _] ? Result : unknown 28 / 36

Slide 33

Slide 33 text

👍

Slide 34

Slide 34 text

デモ

Slide 35

Slide 35 text

工夫点 今回実装に際して工夫していた点 • テストをしっかりと用意した • 型引数のデフォルト値を活用し可読性を担保 29 / 36

Slide 36

Slide 36 text

工夫点 テストをしっかりと用意した 型レベルのテストはコードがコンパイル可能かどうかで判定可能 • コンパイルできる = 型があっている ‣ テスト成功 • コンパイルできない = 型があっていない ‣ テスト失敗 30 / 36

Slide 37

Slide 37 text

工夫点 テストコードの例 function test_JsonParser() { { type Input = '{ "key": 123 }'; type Result = JsonParser; type Expected = { key: 123 }; // コンパイルエラーにならなければOK const _正常系_シンプルなオブジェクト = Equal = true; } } 31 / 36

Slide 38

Slide 38 text

工夫点 型引数のデフォルト値を活用し可読性を担保 型引数を元に以降の型引数のデフォルトを作成できる 変数宣言のように活用 → 名前がつくのでわかりやすく type ParseArrayElements< Tokens extends Token[], ... ParseResult = ParsePrimitiveValue > = ... 32 / 36

Slide 39

Slide 39 text

終わりに

Slide 40

Slide 40 text

やりきれなかったこと • 小数点対応 • ネストされたオブジェクトや配列の対応 • TS の型システムの、無限ループになるかもエラーの解消 暇な時間があればここら辺も実装していきたい 💪 34 / 36

Slide 41

Slide 41 text

感想 • やはり型パズルは楽しい • 最後までできなかった部分はあれど、大分型筋がついた気がする • 今後勉強するものとして、型レベルの計算量などがあると気づいた ‣ 業務でもビルド時間の最適化などにも繋がるかも? ? ? – ほんとかなぁ? 35 / 36

Slide 42

Slide 42 text

リポジトリ 今回のコードは以下のリポジトリに公開しています 実装興味のある方はぜひ見てみてください URL: https://github.com/eraser5th/type-level-json-analyzer (スライド内のコードと異なる場合があります 🙇 36 / 36

Slide 43

Slide 43 text

ご清聴ありがとうございました 👋 良き型ライフを!