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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?