Menu

TypeScriptでJSONを扱う: JSON.parse の型とインターフェース化

JSON.parse は any を返すので、TypeScriptは結果に付けた型をそのまま信じます。パースした JSON に型を付ける方法、型ガードでの検証、JSON のサンプルからインターフェースを作る方法、.json ファイルの import、JSON.stringify が Date や Set や undefined をどう変換するかを解説します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

JSON.parse は any を返すので、TypeScriptは結果を代入した先のどんな型も受け入れます。そのためパースした JSON への型付けは1行で済みますが、その型はあなたがした約束であって、コンパイラーがチェックしたものではありません:

2つ目のオブジェクトの age は文字列 "41" です。any は何にでも代入できるので、TypeScriptはそれでも number とみなし、プログラムは 411 と出力します。直前に自分のコードが書いた JSON なら型注釈で十分です。リクエスト、ファイル、ローカルストレージから来るデータは検証しましょう。

unknown にパースする

結果を unknown として型付けすると、プロパティを使う前にチェックすることをコンパイラーが要求します:

エラーは index.ts(4,13): error TS18046: 'data' is of type 'unknown'. です。そのあと書くチェックのひとつひとつが、data を少しずつ絞り込んでいきます。

型ガードで検証する

型ガードは value is User を返す関数です。true を返すと、TypeScriptはそれ以降その値を User として扱います。中のチェックは本物の実行時のチェックです:

JSON.parse 自体も不正なテキストには SyntaxError を投げるので、実際のコードでは try/catch でも囲みます。大きなデータや入れ子のデータでは手書きの型ガードが長くなります。Zod や Valibot のようなスキーマライブラリを使えば、形を一度宣言するだけで、バリデーターとTypeScriptの型の両方をそこから導けます。

JSON から TypeScriptのインターフェースへ

JSON のサンプルを型に変換するのは機械的な作業です。次のレスポンスがあるとします:

{
    "id": 42,
    "title": "Learn TypeScript",
    "done": false,
    "owner": { "id": 7, "name": "Ada" },
    "tags": ["study", "ts"],
    "dueDate": "2024-03-15T10:30:00Z",
    "notes": null
}

各値をその型に対応させ、入れ子のオブジェクトには独自のインターフェースを与え、変わりうるものに印を付けます:

サンプルがひとつだけでは、どのフィールドが省略可能か、null になりうるかはわかりません。? と | null を決める前に、複数のレスポンスや API のドキュメントを確認しましょう。

日付と reviver

JSON には日付の型がないので、日付は文字列として届きます。JSON.parse の2番目の引数である reviver はキーごとに呼ばれ、日付を作り直せます:

reviver の value パラメーターは any で、結果も any なので、Order 型はチェックされたのではなく信頼されているだけです。JSON.stringify(order, null, 2) は出力をスペース2つでインデントし、Date を ISO 形式の文字列に戻します。

JSON.stringify で失われるもの

JSON.stringify の戻り値の型は string です。変換した値が同じ形で戻ってくるとは限りませんが、型は警告してくれません:

値JSON.stringify のあと
DateISO 形式の文字列(toJSON メソッドによる)
Map, Set{}(先に [...set] や Object.fromEntries(map) で変換する)
オブジェクトの中の undefined、関数、シンボルキーが省かれる
配列の中の undefined、関数、シンボルnull
単独の undefined、関数、シンボル文字列ではなく undefined
NaN, Infinitynull
bigintTypeError を投げる

実行時のルールは普通のJavaScriptと同じで、JavaScriptの JSON で説明しています。

あらゆる JSON の値を表す型

任意の JSON を扱うコードでは、再帰的な型を使うと、JSON が保持できるものを正確に表し、保持できない値を拒否できます:

コメントがなければ、Date オブジェクトは JsonValue ではないので、最後の行はコンパイルエラーになります。

.json ファイルを import する

.json ファイルはモジュールのように import でき、TypeScriptはその内容から型を推論します:

{ "name": "app", "port": 8080, "tags": ["a"] }
// CommonJS output, or a bundler
import config from "./config.json";
const port: number = config.port; // typed from the file: number

// An ES module under module: nodenext
import settings from "./config.json" with { type: "json" };

TypeScript 7 では、module が nodenext、node20、commonjs、esnext、preserve のいずれかなら追加の設定なしで動きます。node16 と node18 では、"resolveJsonModule": true を追加するまではエラー TS2732 Cannot find module './config.json'. Consider using '--resolveJsonModule' to import module with '.json' extension. で失敗します。これを false にすると、どこでも JSON の import が無効になります。nodenext や node20 の ES モジュールでは、import に with { type: "json" } 属性が必要で(ないとエラー TS1543)、デフォルトの import しか使えません(import { port } はエラー TS1544)。tsc は import された .json ファイルを、コンパイルしたJavaScriptと並べて outDir にコピーします。

よくある質問

TypeScriptの JSON.parse は何の型を返しますか?

any です。コンパイラーには文字列の中身がわからないので、const user: User = JSON.parse(text) はテキストに何が入っていてもコンパイルが通ります。データがプログラムの外から来る場合は、結果を unknown に代入して検証しましょう。

JSON を TypeScriptのインターフェースに変換するには?

代表的なサンプルを用意し、キーごとにプロパティを書きます。文字列、数値、真偽値は string、number、boolean に対応し、入れ子のオブジェクトは独自のインターフェースに、オブジェクトの配列は Item[] になり、ないことがあるキーには ? を付けます。quicktype のようなコード生成ツールで自動化できますが、推測された型は複数のサンプルで確認してください。

TypeScriptで JSON ファイルを import するには?

TypeScript 7 では、module が nodenext、node20、commonjs、esnext、preserve のいずれかなら import config from "./config.json"; が使え、結果にはファイルの内容から型が付きます。node16 や node18 では "resolveJsonModule": true も設定します。nodenext や node20 の ES モジュールでは、Node が要求する属性を付けます: import config from "./config.json" with { type: "json" };。

JSON.stringify は常に文字列を返しますか?

型は string ですが、実行時には JSON.stringify(undefined) と JSON.stringify(() => 1) は undefined を返します。オブジェクトの中の値も変換され、Date は ISO 形式の文字列に、Map と Set は {} になります。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める