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 のあと |
|---|---|
Date | ISO 形式の文字列(toJSON メソッドによる) |
Map, Set | {}(先に [...set] や Object.fromEntries(map) で変換する) |
オブジェクトの中の undefined、関数、シンボル | キーが省かれる |
配列の中の undefined、関数、シンボル | null |
単独の undefined、関数、シンボル | 文字列ではなく undefined |
NaN, Infinity | null |
bigint | TypeError を投げる |
実行時のルールは普通の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 は {} になります。