- TypeScript
TypeScript JSON.parseの型付け unknownと検証コード
TypeScript の JSON.parse は any を返すので、型安全に使うには、戻り値を unknown で受けて、中身を検証してから目的の型として扱います。 as User や const user: User = と書くだけでは tsc は何も調べず、形の違う JSON もそのまま通ります。
この記事では、as で断定・型注釈・手書きの型ガード・assertion 関数・検証ライブラリ(zod)の5方式を、同じ「壊れた JSON」5種類に通した結果を対照表にしました。JSON.parse、fetch、ファイル読み込み、JSON の import がそれぞれ何の型を返すかの一覧と、そのまま貼って使える safeParse のコードも載せています。
確認環境:Ubuntu 24.04 / TypeScript 7.0.2 / Node.js 22.22.2 と 24.21.0 / zod 4.6.5 / @types/node 22.20.5。TypeScript のファイルは node ファイル名.ts でそのまま実行しています。
JSON.parseの型付けの早見表
| 方式 | 書き方 | 形の違う JSON | 向いている場面 |
|---|---|---|---|
| 1. as で断定 | JSON.parse(text) as User |
通してしまう | 自分で直前に作った文字列を読み戻すとき |
| 2. 型注釈 | const u: User = JSON.parse(text) |
通してしまう | 同上(as と同じ結果) |
| 3. 型ガード | if (isUser(data)) |
入口で止める | 分岐して続けたいとき。ライブラリなしで書ける |
| 4. assertion 関数 | assertUser(data) |
入口で止める(理由つき) | 違っていたら例外で止めたいとき |
| 5. 検証ライブラリ | UserSchema.parse(data) |
入口で止める(場所と理由つき) | 項目が多い、入れ子が深い、型と検証を1か所にまとめたいとき |
| JSON の入口 | 戻り値の型(tsc 7.0.2) |
|---|---|
JSON.parse(text) |
any |
await response.json() |
any |
await readFile(path, "utf8") |
string(JSON.parse に渡すと any) |
import user from "./user.json" with { type: "json" } |
ファイルの中身から推論された型 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「TypeScript JSON.parseの型付け unknownと検証コード」
JSONの入口ごとの戻り値の型を確かめる
JSON が入ってくる場所ごとに、TypeScript が何の型を付けるかを確かめます。次のコードを tsc の --declaration で型定義ファイルに出力しました。
import { readFile } from "node:fs/promises";
import { readFileSync } from "node:fs";
import user from "./user.json" with { type: "json" };
const text = '{"id":1,"name":"taro","tags":["admin"]}';
const res = await fetch("http://127.0.0.1:9420/user.json");
export const a = JSON.parse(text);
export const b = JSON.parse(text, (key, value) => value);
export const c = await res.json();
export const d = await res.text();
export const e = await readFile("user.json", "utf8");
export const f = readFileSync("user.json", "utf8");
export const g = JSON.parse(await readFile("user.json", "utf8"));
export const h = user;
export const i = await import("./user.json", { with: { type: "json" } });
export const j = structuredClone(user);
export const k = JSON.stringify(user);
export const l = await new Request("http://127.0.0.1:9420/", { method: "POST", body: text }).json();
user.json の中身は { "id": 1, "name": "taro", "tags": ["admin", "staff"] } です。出力された型は次のとおりでした。
export declare const a: any;
export declare const b: any;
export declare const c: any;
export declare const d: string;
export declare const e: string;
export declare const f: string;
export declare const g: any;
export declare const h: {
id: number;
name: string;
tags: string[];
};
export declare const i: {
default: {
id: number;
name: string;
tags: string[];
};
};
export declare const j: {
id: number;
name: string;
tags: string[];
};
export declare const k: string;
export declare const l: any;
| 入口 | 型 | 補足 |
|---|---|---|
JSON.parse(reviver あり・なし) |
any |
lib.es5.d.ts の定義は parse(text: string, reviver?: (this: any, key: string, value: any) => any): any |
response.json()、request.json() |
Promise<any> |
await すると any |
response.text() |
Promise<string> |
文字列で受けてから自分で JSON.parse できる |
readFile(path, "utf8")、readFileSync |
string |
文字コードを省くと Buffer になる |
| JSON の import(静的) | 中身から推論 | "taro" は string、配列は string[] に広げられる |
| JSON の import(動的) | { default: … } |
.default に中身が入る |
実行時に外から届く JSON は、どの入口でも any になります。 型が付くのは、ビルド時に中身が決まっている import だけです。
anyのままだと何が起きるか、unknownにすると何が変わるか
type User = { id: number; name: string; tags: string[] };
const text = '{"id":1,"name":"taro","tags":["admin"]}';
const a = JSON.parse(text);
console.log(a.nmae.toUpperCase());
const b: unknown = JSON.parse(text);
console.log(b.name);
const c: User = b;
const d: User = JSON.parse(text);
const e = JSON.parse(text) as User;
console.log(d.name, e.name);
$ npx tsc -p .
src/unknown.ts(8,13): error TS18046: 'b' is of type 'unknown'.
src/unknown.ts(10,7): error TS2322: Type 'unknown' is not assignable to type 'User'.
- 5行目の
a.nmaeはつづりの間違いですが、any なのでエラーになりません。 - unknown で受けた b は、プロパティを読むだけで TS18046 になり、User への代入も TS2322 で止まります。検証を書くまで先へ進めない状態を、tsc が作ってくれます。
- 12〜13行目の型注釈と as は、どちらもエラーなしで通ります。any からはどんな型にも代入できるためです。
5方式を同じ壊れたJSONに通して比べる
5つの方式で User を取り出し、そのあと「id に 1 を足す、name を大文字にする、tags を大文字にしてつなぐ」という処理に渡します。入力は、正しい JSON 1つと、壊れた JSON 5種類です。
import { z } from "zod";
type User = { id: number; name: string; tags: string[] };
const inputs: [string, string][] = [
["正しいJSON", '{"id":1,"name":"taro","tags":["admin"]}'],
["キー欠落", '{"id":1,"tags":["admin"]}'],
["型違い", '{"id":"1","name":"taro","tags":["admin"]}'],
["null", "null"],
["配列の中身違い", '{"id":1,"name":"taro","tags":["admin",7]}'],
["構文エラー", '{"id":1,"name":"taro"'],
];
// 1. as で断定
function byAs(text: string): User {
return JSON.parse(text) as User;
}
// 2. 型注釈
function byAnnotation(text: string): User {
const user: User = JSON.parse(text);
return user;
}
// 3. 手書きの型ガード
function isUser(v: unknown): v is User {
return (
typeof v === "object" && v !== null &&
"id" in v && typeof v.id === "number" &&
"name" in v && typeof v.name === "string" &&
"tags" in v && Array.isArray(v.tags) &&
v.tags.every((t) => typeof t === "string")
);
}
function byGuard(text: string): User {
const data: unknown = JSON.parse(text);
if (!isUser(data)) throw new Error("User の形ではありません");
return data;
}
// 4. assertion 関数
function assertUser(v: unknown): asserts v is User {
if (typeof v !== "object" || v === null) throw new TypeError("オブジェクトではありません");
if (!("id" in v) || typeof v.id !== "number") throw new TypeError("id が number ではありません");
if (!("name" in v) || typeof v.name !== "string") throw new TypeError("name が string ではありません");
if (!("tags" in v) || !Array.isArray(v.tags)) throw new TypeError("tags が配列ではありません");
v.tags.forEach((t, i) => {
if (typeof t !== "string") throw new TypeError(`tags[${i}] が string ではありません`);
});
}
function byAssert(text: string): User {
const data: unknown = JSON.parse(text);
assertUser(data);
return data;
}
// 5. 検証ライブラリ(zod)
const UserSchema = z.object({
id: z.number(),
name: z.string(),
tags: z.array(z.string()),
});
function byZod(text: string): User {
return UserSchema.parse(JSON.parse(text));
}
// 受け取った User を使う側(型の上では何も問題がないコード)
function use(user: User): string {
const next = user.id + 1;
const upper = user.name.toUpperCase();
const tags = user.tags.map((t) => t.toUpperCase()).join(",");
return `次のid=${next} 名前=${upper} タグ=${tags}`;
}
const methods: [string, (text: string) => User][] = [
["1.as", byAs], ["2.型注釈", byAnnotation], ["3.型ガード", byGuard],
["4.assertion", byAssert], ["5.zod", byZod],
];
const short = (e: unknown) =>
e instanceof z.ZodError
? `ZodError: ${e.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join(" / ")}`
: String(e);
console.log(`Node.js ${process.version}`);
for (const [label, text] of inputs) {
console.log(`\n■ ${label} ${text}`);
for (const [name, parse] of methods) {
let user: User;
try {
user = parse(text);
} catch (e) {
console.log(` ${name}: 入口で止めた → ${short(e)}`);
continue;
}
try {
console.log(` ${name}: 通した → ${use(user)}`);
} catch (e) {
console.log(` ${name}: 通した → 使う側で ${short(e)}`);
}
}
}
このファイルは npx tsc -p . がエラーなしで通ります。実行結果です。
$ node src/compare.ts
Node.js v22.22.2
■ 正しいJSON {"id":1,"name":"taro","tags":["admin"]}
1.as: 通した → 次のid=2 名前=TARO タグ=ADMIN
2.型注釈: 通した → 次のid=2 名前=TARO タグ=ADMIN
3.型ガード: 通した → 次のid=2 名前=TARO タグ=ADMIN
4.assertion: 通した → 次のid=2 名前=TARO タグ=ADMIN
5.zod: 通した → 次のid=2 名前=TARO タグ=ADMIN
■ キー欠落 {"id":1,"tags":["admin"]}
1.as: 通した → 使う側で TypeError: Cannot read properties of undefined (reading 'toUpperCase')
2.型注釈: 通した → 使う側で TypeError: Cannot read properties of undefined (reading 'toUpperCase')
3.型ガード: 入口で止めた → Error: User の形ではありません
4.assertion: 入口で止めた → TypeError: name が string ではありません
5.zod: 入口で止めた → ZodError: name: Invalid input: expected string, received undefined
■ 型違い {"id":"1","name":"taro","tags":["admin"]}
1.as: 通した → 次のid=11 名前=TARO タグ=ADMIN
2.型注釈: 通した → 次のid=11 名前=TARO タグ=ADMIN
3.型ガード: 入口で止めた → Error: User の形ではありません
4.assertion: 入口で止めた → TypeError: id が number ではありません
5.zod: 入口で止めた → ZodError: id: Invalid input: expected number, received string
■ null null
1.as: 通した → 使う側で TypeError: Cannot read properties of null (reading 'id')
2.型注釈: 通した → 使う側で TypeError: Cannot read properties of null (reading 'id')
3.型ガード: 入口で止めた → Error: User の形ではありません
4.assertion: 入口で止めた → TypeError: オブジェクトではありません
5.zod: 入口で止めた → ZodError: (root): Invalid input: expected object, received null
■ 配列の中身違い {"id":1,"name":"taro","tags":["admin",7]}
1.as: 通した → 使う側で TypeError: t.toUpperCase is not a function
2.型注釈: 通した → 使う側で TypeError: t.toUpperCase is not a function
3.型ガード: 入口で止めた → Error: User の形ではありません
4.assertion: 入口で止めた → TypeError: tags[1] が string ではありません
5.zod: 入口で止めた → ZodError: tags.1: Invalid input: expected string, received number
■ 構文エラー {"id":1,"name":"taro"
1.as: 入口で止めた → SyntaxError: Expected ',' or '}' after property value in JSON at position 21 (line 1 column 22)
2.型注釈: 入口で止めた → SyntaxError: Expected ',' or '}' after property value in JSON at position 21 (line 1 column 22)
3.型ガード: 入口で止めた → SyntaxError: Expected ',' or '}' after property value in JSON at position 21 (line 1 column 22)
4.assertion: 入口で止めた → SyntaxError: Expected ',' or '}' after property value in JSON at position 21 (line 1 column 22)
5.zod: 入口で止めた → SyntaxError: Expected ',' or '}' after property value in JSON at position 21 (line 1 column 22)
Node.js 24.21.0 でも、1行目のバージョン表示以外は同じ出力でした。
結果の対照表
| 壊れ方 | 1. as | 2. 型注釈 | 3. 型ガード | 4. assertion | 5. zod |
|---|---|---|---|---|---|
| キー欠落 | 通す → 使う側で TypeError | 通す → 使う側で TypeError | 止める | 止める(name) | 止める(name) |
| 型違い(id が文字列) | 通す → id が 11 になる | 通す → id が 11 になる | 止める | 止める(id) | 止める(id) |
| null | 通す → 使う側で TypeError | 通す → 使う側で TypeError | 止める | 止める | 止める(root) |
| 配列の中身違い | 通す → 使う側で TypeError | 通す → 使う側で TypeError | 止める | 止める(tags[1]) | 止める(tags.1) |
| 構文エラー | 止まる(SyntaxError) | 止まる(SyntaxError) | 止まる(SyntaxError) | 止まる(SyntaxError) | 止まる(SyntaxError) |
読み取れることは3つです。
- as と型注釈は、実行結果がまったく同じ。 どちらも tsc への宣言であって、実行時の検証ではありません。
- 気づきにくいのは「型違い」の行。
"1" + 1は文字列の連結になり、エラーも出ないまま次のid=11という値が先へ進みます。TypeError で止まる行より、こちらのほうが原因を探しにくくなります。 - 構文エラーは JSON.parse 自身が SyntaxError を投げるので、5方式とも同じ。 形の検証とは別に try...catch が必要です。
型ガード・assertion・zod の違いは、止めたときに「どこが違うか」がどれだけ伝わるかです。型ガードは true か false だけ、assertion は自分で書いたメッセージ、zod は項目の場所(tags.1)と期待した型まで出ます。
コピペで使えるsafeParse(Resultを返す)
「構文エラー」と「形の違い」の両方を、例外ではなく戻り値で受け取れるようにした関数です。ok が true のときだけ value が使える形(Result)にすると、確認を書き忘れたときに tsc が教えてくれます。
safe-json.ts
import { readFile } from "node:fs/promises";
export type Result<T> =
| { ok: true; value: T }
| { ok: false; error: string };
export type Guard<T> = (value: unknown) => value is T;
/** 文字列を読み、guard を通ったものだけを T として返す */
export function safeParse<T>(text: string, guard: Guard<T>): Result<T> {
let data: unknown;
try {
data = JSON.parse(text);
} catch (e) {
return { ok: false, error: `JSONとして読めません: ${e instanceof Error ? e.message : String(e)}` };
}
if (!guard(data)) {
return { ok: false, error: "JSONの形が想定と違います" };
}
return { ok: true, value: data };
}
/** fetch 用:通信・ステータス・JSON・形の4段階をすべて Result にまとめる */
export async function fetchJson<T>(url: string, guard: Guard<T>): Promise<Result<T>> {
let res: Response;
try {
res = await fetch(url);
} catch (e) {
return { ok: false, error: `通信できません: ${e instanceof Error ? e.message : String(e)}` };
}
if (!res.ok) {
return { ok: false, error: `HTTP ${res.status}` };
}
return safeParse(await res.text(), guard);
}
/** ファイル用 */
export async function readJsonFile<T>(path: string, guard: Guard<T>): Promise<Result<T>> {
let text: string;
try {
text = await readFile(path, "utf8");
} catch (e) {
return { ok: false, error: `ファイルを読めません: ${e instanceof Error ? e.message : String(e)}` };
}
return safeParse(text, guard);
}
fetchJson で res.json() ではなく res.text() を使っているのは、JSON として読めなかった場合も safeParse の中でまとめて扱うためです。
main.ts(使う側。確認用のサーバーを同じファイルの中で起動しています)
import { createServer } from "node:http";
import { fetchJson, readJsonFile, safeParse } from "./safe-json.ts";
type User = { id: number; name: string; tags: string[] };
function isUser(v: unknown): v is User {
return (
typeof v === "object" && v !== null &&
"id" in v && typeof v.id === "number" &&
"name" in v && typeof v.name === "string" &&
"tags" in v && Array.isArray(v.tags) &&
v.tags.every((t) => typeof t === "string")
);
}
// 1. 文字列
console.log(safeParse('{"id":1,"name":"taro","tags":["admin"]}', isUser));
console.log(safeParse('{"id":"1","name":"taro","tags":[]}', isUser));
console.log(safeParse('{"id":1,', isUser));
// ok を確かめると value の型が User に絞られる
const r = safeParse('{"id":7,"name":"hanako","tags":[]}', isUser);
if (r.ok) {
console.log(r.value.name.toUpperCase());
} else {
console.log(r.error);
}
// 2. fetch(確認用のサーバーをポート 9420 で起動)
const server = createServer((req, res) => {
if (req.url === "/user") {
res.writeHead(200, { "content-type": "application/json" });
res.end('{"id":2,"name":"jiro","tags":["staff"]}');
} else if (req.url === "/html") {
res.writeHead(200, { "content-type": "text/html" });
res.end("<!doctype html><title>Login</title>");
} else {
res.writeHead(404, { "content-type": "application/json" });
res.end('{"message":"not found"}');
}
}).listen(9420);
await new Promise((ok) => server.once("listening", ok));
console.log(await fetchJson("http://127.0.0.1:9420/user", isUser));
console.log(await fetchJson("http://127.0.0.1:9420/html", isUser));
console.log(await fetchJson("http://127.0.0.1:9420/none", isUser));
console.log(await fetchJson("http://127.0.0.1:9421/user", isUser));
// 参考:response.json() をそのまま呼んだ場合
try {
const res = await fetch("http://127.0.0.1:9420/html");
await res.json();
} catch (e) {
console.log(String(e));
}
server.close();
// 3. ファイル
console.log(await readJsonFile("src/user.json", isUser));
console.log(await readJsonFile("src/none.json", isUser));
$ node src/main.ts
{ ok: true, value: { id: 1, name: 'taro', tags: [ 'admin' ] } }
{ ok: false, error: 'JSONの形が想定と違います' }
{
ok: false,
error: 'JSONとして読めません: Expected double-quoted property name in JSON at position 8 (line 1 column 9)'
}
HANAKO
{ ok: true, value: { id: 2, name: 'jiro', tags: [ 'staff' ] } }
{
ok: false,
error: `JSONとして読めません: Unexpected token '<', "<!doctype "... is not valid JSON`
}
{ ok: false, error: 'HTTP 404' }
{ ok: false, error: '通信できません: fetch failed' }
SyntaxError: Unexpected token '<', "<!doctype "... is not valid JSON
{
ok: true,
value: { id: 1, name: 'taro', tags: [ 'admin', 'staff' ] }
}
{
ok: false,
error: "ファイルを読めません: ENOENT: no such file or directory, open 'src/none.json'"
}
Node.js 22.22.2 と 24.21.0 で同じ出力でした。fetch で起きる4種類(成功、HTML が返ってきた、404、接続できない)が、すべて同じ形の戻り値で受け取れています。JSON を期待した URL から HTML が返ると、Unexpected token '<' という SyntaxError になる点は、よく出会うので覚えておくと原因にすぐ気づけます。
import で ./safe-json.ts と拡張子まで書いているのは、Node.js で .ts をそのまま実行するためです。tsc 側は tsconfig.json に "allowImportingTsExtensions": true を入れています。
検証の中身をzodに任せる
項目が増えて型ガードを手で書くのが大変になったら、guard の中身だけ zod に置き換えられます。型も z.infer でスキーマから作れるので、型と検証がずれません。
import { z } from "zod";
import { safeParse } from "./safe-json.ts";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
tags: z.array(z.string()),
});
type User = z.infer<typeof UserSchema>;
const isUser = (v: unknown): v is User => UserSchema.safeParse(v).success;
console.log(safeParse('{"id":1,"name":"taro","tags":["admin"]}', isUser));
console.log(safeParse('{"id":1,"name":"taro"}', isUser));
// 理由まで知りたいときは zod の safeParse をそのまま使う
const r = UserSchema.safeParse(JSON.parse('{"id":1,"name":"taro"}'));
if (!r.success) {
console.log(r.error.issues);
}
$ node src/zodguard.ts
{ ok: true, value: { id: 1, name: 'taro', tags: [ 'admin' ] } }
{ ok: false, error: 'JSONの形が想定と違います' }
[
{
expected: 'array',
code: 'invalid_type',
path: [ 'tags' ],
message: 'Invalid input: expected array, received undefined'
}
]
JSONファイルをimportで読むときの設定と型
設定ファイルのようにビルド時に中身が決まっている JSON は、import で読むと型が自動で付きます。
import user from "./user.json" with { type: "json" };
console.log(user.name.toUpperCase(), user.tags.length);
$ node src/import.ts
TARO 2
with { type: "json" } を省くと、"module": "nodenext" の設定では次のエラーになります。
src/noattr.ts(1,18): error TS1543: Importing a JSON file into an ECMAScript module requires a 'type: "json"' import attribute when 'module' is set to 'NodeNext'.
この記事で使った tsconfig.json です。
{
"compilerOptions": {
"target": "es2022",
"module": "nodenext",
"lib": ["es2022", "dom"],
"types": ["node"],
"strict": true,
"resolveJsonModule": true,
"rootDir": "src",
"noEmit": true,
"allowImportingTsExtensions": true
},
"include": ["src"]
}
この設定から "resolveJsonModule": true の行を消しても、tsc 7.0.2 はエラーなしで通りました。"module": "nodenext" では JSON の import がはじめから使える状態になっています。
| 読み方 | 型 | 中身が変わったとき | 向いているもの |
|---|---|---|---|
| import | 自動で付く | ビルドし直すまで反映されない | アプリに同梱する設定、固定の一覧データ |
| readFile と safeParse | 検証して付ける | 実行のたびに読み直す | 利用者が編集するファイル、実行時に差し替えるデータ |
reviverで値を変換するときの型
JSON には日付の型がないので、日付は文字列で届きます。JSON.parse の第2引数(reviver)で Date に変換できますが、reviver を使っても戻り値は any のままです。変換後の形を型ガードで確かめます。
type Task = { title: string; due: Date };
const text = '{"title":"資料作成","due":"2026-10-05T09:00:00.000Z"}';
const plain = JSON.parse(text);
console.log(typeof plain.due, plain.due);
const revived: unknown = JSON.parse(text, (key, value) =>
key === "due" && typeof value === "string" ? new Date(value) : value
);
function isTask(v: unknown): v is Task {
return (
typeof v === "object" && v !== null &&
"title" in v && typeof v.title === "string" &&
"due" in v && v.due instanceof Date && !Number.isNaN(v.due.getTime())
);
}
if (isTask(revived)) {
console.log(revived.due instanceof Date, revived.due.getFullYear());
}
console.log(isTask(plain));
$ node src/reviver.ts
string 2026-10-05T09:00:00.000Z
true 2026
false
reviver を通さない plain は due が文字列のままなので、isTask(plain) は false になります。JSON.parse(text) as Task と書くと、型は Date、中身は文字列という食い違いが起き、getFullYear を呼んだところで止まります。
どの方式を選ぶかの判断表
| 状況 | 選ぶ方式 | 理由 |
|---|---|---|
| 自分のコードが直前に JSON.stringify した文字列を読む | as または型注釈 | 形を決めたのが自分自身 |
| 外部の API、利用者が編集するファイル、localStorage | 型ガード、assertion、zod のいずれか | 形を決めたのが自分以外 |
| 項目が数個で、依存を増やしたくない | 型ガード+safeParse | 20行ほどで書ける |
| 違っていたら処理全体を止めたい(起動時の設定読み込みなど) | assertion 関数 | 理由つきの例外でその場で止まる |
| 項目が多い、入れ子が深い、エラーの場所を利用者に見せたい | zod などのライブラリ | 場所と理由が自動で出る。型もスキーマから作れる |
| ビルド時に中身が決まっている | import | 型が自動で付き、検証を書かなくてよい |
ジェネリクスで型を渡す次のような関数も tsc は通りますが、中で何も検証していないので、結果は as と同じです。
async function getJson<T>(url: string): Promise<T> {
const res = await fetch(url);
return res.json();
}
よくある質問
Q. JSON.parse(text) as User は避けたほうがよいですか?
A. 形を自分で保証できる場面なら問題ありません。外から届く JSON に使うと、今回の表のとおり、形が違ってもそのまま進みます。入口で一度だけ検証し、その先は User 型として安心して使う、という分け方がおすすめです。
Q. 型ガードと assertion 関数はどう使い分けますか? A. 違っていた場合にも処理を続けたい(既定値を使う、画面にメッセージを出す)なら型ガード、続ける意味がないなら assertion 関数が向いています。assertion 関数は if を書かずに次の行から型が絞られます。
Q. 型ガードの中身を書き間違えるとどうなりますか?
A. v is User は「true を返したら User として扱う」という約束なので、確認が足りないまま true を返すと as と同じ状態になります。項目を足したら型ガードも直す必要があるため、項目が多い型は zod のように型と検証を1か所で定義できる方法が合います。
Q. response.json() に型引数は渡せますか?
A. 渡せません。定義は json(): Promise<any> で、res.json<User>() と書くと TS2558(Expected 0 type arguments, but got 1.)になります。unknown で受けて検証するか、この記事の fetchJson のように text() で受けて safeParse に渡します。
まとめ
- JSON.parse と response.json() の戻り値は any。型注釈や as を書いても、実行時には何も確かめられない
- as と型注釈は、5種類の壊れた JSON すべてで同じ結果。「型違い」はエラーも出ずに
id=11のような値が先へ進む const data: unknown = JSON.parse(text)と受けると、検証するまで TS18046 で止まる- 型ガード・assertion 関数・zod は入口で止める。違いは、止めた理由がどこまで伝わるか
- 構文エラーは形の検証とは別。try...catch と合わせて Result で返す safeParse にまとめると扱いやすい
- fetch は text() で受けて safeParse に渡すと、通信・ステータス・JSON・形の4段階を同じ形で受け取れる
- ビルド時に決まっている JSON は
with { type: "json" }の import で型が自動で付く
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「TypeScript JSON.parseの型付け unknownと検証コード」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。