$30 off During Our Annual Pro Sale. View Details »
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
TypeScriptで型レベルJSONパーサー
Search
kato-takeshi1
October 02, 2025
0
340
TypeScriptで型レベルJSONパーサー
JSON文字列のリテラル型を解析してTypeScriptに変換するやつ作りました
バグは多いですがおおかた動くくらいです
kato-takeshi1
October 02, 2025
Tweet
Share
Featured
See All Featured
Raft: Consensus for Rubyists
vanstee
140
7.2k
CoffeeScript is Beautiful & I Never Want to Write Plain JavaScript Again
sstephenson
162
15k
Build your cross-platform service in a week with App Engine
jlugia
234
18k
How to train your dragon (web standard)
notwaldorf
97
6.4k
Creating an realtime collaboration tool: Agile Flush - .NET Oxford
marcduiker
35
2.3k
Testing 201, or: Great Expectations
jmmastey
46
7.8k
Building an army of robots
kneath
306
46k
It's Worth the Effort
3n
187
29k
Sharpening the Axe: The Primacy of Toolmaking
bcantrill
46
2.6k
Designing for Performance
lara
610
69k
実際に使うSQLの書き方 徹底解説 / pgcon21j-tutorial
soudai
PRO
196
69k
Large-scale JavaScript Application Architecture
addyosmani
514
110k
Transcript
TypeScript で型レベル JSON パーサ Think! FrontEnd #8 合同会社 DMM.com 電子書籍開発部
25 新卒 加藤 豪
自己紹介 本名 加藤 豪 ニックネーム ERASER 大学 会津大学 領域 Web
FE 趣味 ゲーム、イラスト見る、VTuber 所属 二次元開発本部 電子書籍開発部 1 / 36
本日のお品書き 1. はじめに • 今回作ったもの • デモ 2. 概要 •
JSON パーサの構成 • Tokenizer とは • Parser とは 3. 実装 • Tokenizer の実装 • Parser の実装 • ところどころで実装テクニックを解説 4. 終わりに • やりきれなかったこと • 感想 2 / 36
はじめに
今回作ったもの 型レベル JSON パーサ 文字列リテラル型を JSON として解析する 型 type Input
= '{ "key": 123 }'; type Result = JsonParser<Input>; type Expected = { key: 123 }; 4 / 36
型…??? 🤔 様子がおかしいな…
はじめに ❌ JSON を解析する実行プログラム ⭕ JSON を解析する型 5 / 36
期待している方にお断り 1. 型なのでコンパイル時に JSON がわかってないと使えない • 動的に JSON を解析することはできない •
例えば API のレスポンスを解析することはできない 2. JSON の仕様に準拠しているわけではない • 適当です 6 / 36
概要
JSON パーサの構成 作成した JSON パーサの構成は大きく 2 つに分かれる • Tokenizer 字句解析
入力文字列をトークンに分割 • Parser 構文解析 トークン列型を解析して最終的な型に変換する 8 / 36
Tokenizer とは 文字列をトークンに分割する トークン 意味のある最小単位の文字列を表すもの • 例: ‣ 1234 ->
NumberToken<1234> ‣ "hello" -> StringToken<"hello"> ‣ { -> LeftBraceToken 9 / 36
Tokenizer とは 文字列をトークンに分割する '{ "key": 123 }' ↓ 分割 •
{ LeftBraceToken • “key” StringToken<"key"> • : ColonToken • 123 NumberToken<123> • } RightBraceToken 10 / 36
Parser とは トークン列型を解析して 最終的な型に変換する • { LeftBraceToken • “key” StringToken<"key">
• : ColonToken • 123 NumberToken<123> • } RightBraceToken ↓ 解析 { key: 123 } 11 / 36
実装
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
Tokenizer の実装(1/4) … 小さな Tokenizer まずは個別のトークンを抽出する 小さな Tokenizer を実装 これらはトークンを発見し以下を返す
• 抽出したトークン • 消費文字数 14 / 36
Tokenizer の実装(1/4) … 小さな Tokenizer 例えば true を抽出する TrueTokenizer は以下のように実装
type TrueTokenizer<S extends string> = S extends `true${string}` ? [TrueToken, 4] : never; 15 / 36
ちょっと解説 1 TS の型演算には if 文が存在しない extends を使うと条件分岐ができる 型1 extends
型2 ? 分岐1 : 分岐2 16 / 36
ちょっと解説 2 以下のようなものを template literal types と言います `true${string}` これは true
で始まる任意の文字列型にマッチします 17 / 36
Tokenizer の実装(2/4) 小さな Tokenizer を組み合わせて トークンを 1 つ抽出する TokenizeOnce を実装
type TokenizeOnce<S extends string> = | RightBraceTokenizer<S> | LeftBraceTokenizer<S> | ColonTokenizer<S> | TrueTokenizer<S> | FalseTokenizer<S> | WhiteSpaceTokenizer<S> | NullTokenizer<S> | LeftSquareBracketTokenizer<S> | RightSquareBracketTokenizer<S> | StringTokenizer<S> | CommaTokenizer<S> | NumberTokenizer<S> 18 / 36
Tokenizer の実装(3/4) … ループ処理 TokenizeOnce を使い文字列をループ処理して 文字列全体をトークン列に変換する TokenizerInner を実装 実装は複雑なので割愛します
コードは載せておくので興味があれば見てください 19 / 36
Tokenizer の実装(3/4) … ループ処理 実際のコード type TokenizerInner< S extends string,
AccTokens extends Token[] = [], End = EndTokenizer<S> extends never ? false : true, Result extends [Token, number] = TokenizeOnce<S>, 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<SkipString<S, ReadedLength>, [...AccTokens, NextToken]>; 20 / 36
ちょっと解説 3 TS の型演算には for や while が存在しない ただし再帰関数よろしく型を再起的に呼び出せる type
MakeTupleN< T extends unknown, N extends number, Acc extends unknown[] = [] > = Acc['length'] extends N ? Acc : MakeTupleN<T, N, [T, ...Acc]>; // 再帰 Listing 1: 長さ N のタプル型を作成 21 / 36
Tokenizer の実装(4/4) … 完成 型引数を隠蔽して完成! ! ! ! export type
Tokenizer<S extends string> = TokenizerInner<S>; 22 / 36
ふぅ…
Parser の実装(1/5) 特定の値を読み取る小さなパーサを実装 type ParseStringValue<Tokens extends Token[]> = Tokens extends
[StringToken<infer S>, ...infer Remain] ? [S, Remain] : never 23 / 36
Parser の実装(2/5) 小さなパーサを組み合わせてプリミティブな値(配列,オブジェクト以外) を読み取る ParsePrimitiveValue を実装 export type ParsePrimitiveValue<Tokens extends
Token[]> = | ParseStringValue<Tokens> | ParseNumberValue<Tokens> | ParseBooleanValue<Tokens> | ParseNullValue<Tokens> | EncounteredBad<Tokens> 24 / 36
Parser の実装(3/5) オブジェクトをパースする ParseObject の実装 本実装は長いので割愛 export type ParseObject<Tokens extends
Token[]> = Tokens extends [SimpleToken.LeftBrace, SimpleToken.RightBrace, ...infer RemainTokens extends Token[]] ? [{}, RemainTokens] : Tokens extends [SimpleToken.LeftBrace, ...infer RemainTokens extends Token[]] ? ParseFields<RemainTokens> : never 25 / 36
Parser の実装(4/5) 配列をパースする ParseArray の実装 本実装は例によって割愛 export type ParseArray<Tokens extends
Token[]> = Tokens extends [SimpleToken.LeftSquareBracket, ...infer RemainTokens extends Token[]] ? ParseArrayElements<RemainTokens> : never 26 / 36
Parser の実装(5/5) ここまで作ったものを組み合わせて Parser の完成 export type Parser<T extends Token[]>
= | ParseObject<T> | ParseArray<T> | ParsePrimitiveValue<T>; 27 / 36
そして
そして Json パーサの完成 出来上がった Tokenizer と Parser を組み合わせて完成! ! !
export type JsonParser<S extends string = ""> = Parser< ArrayRemoveElement<Tokenizer<S>, SimpleToken.WhiteSpace> > extends [infer Result, ...infer _] ? Result : unknown 28 / 36
👍
デモ
工夫点 今回実装に際して工夫していた点 • テストをしっかりと用意した • 型引数のデフォルト値を活用し可読性を担保 29 / 36
工夫点 テストをしっかりと用意した 型レベルのテストはコードがコンパイル可能かどうかで判定可能 • コンパイルできる = 型があっている ‣ テスト成功 •
コンパイルできない = 型があっていない ‣ テスト失敗 30 / 36
工夫点 テストコードの例 function test_JsonParser() { { type Input = '{
"key": 123 }'; type Result = JsonParser<Input>; type Expected = { key: 123 }; // コンパイルエラーにならなければOK const _正常系_シンプルなオブジェクト = Equal<Result, Expected> = true; } } 31 / 36
工夫点 型引数のデフォルト値を活用し可読性を担保 型引数を元に以降の型引数のデフォルトを作成できる 変数宣言のように活用 → 名前がつくのでわかりやすく type ParseArrayElements< Tokens extends
Token[], ... ParseResult = ParsePrimitiveValue<Tokens> > = ... 32 / 36
終わりに
やりきれなかったこと • 小数点対応 • ネストされたオブジェクトや配列の対応 • TS の型システムの、無限ループになるかもエラーの解消 暇な時間があればここら辺も実装していきたい 💪
34 / 36
感想 • やはり型パズルは楽しい • 最後までできなかった部分はあれど、大分型筋がついた気がする • 今後勉強するものとして、型レベルの計算量などがあると気づいた ‣ 業務でもビルド時間の最適化などにも繋がるかも? ?
? – ほんとかなぁ? 35 / 36
リポジトリ 今回のコードは以下のリポジトリに公開しています 実装興味のある方はぜひ見てみてください URL: https://github.com/eraser5th/type-level-json-analyzer (スライド内のコードと異なる場合があります 🙇 36 / 36
ご清聴ありがとうございました 👋 良き型ライフを!