Menu

JSON в TypeScript: тип JSON.parse, JSON в интерфейс, импорт

JSON.parse возвращает any, поэтому TypeScript верит любому типу, который вы дадите результату. Как типизировать разобранный JSON, проверить его защитником типа, превратить пример JSON в интерфейс, импортировать файлы .json и что JSON.stringify делает с Date, Set и undefined.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

JSON.parse возвращает any, поэтому TypeScript принимает любой тип, которому вы присваиваете результат. Так что типизировать разобранный JSON можно одной строкой, но это значит и то, что тип это ваше обещание, а не то, что проверяет компилятор:

Во втором объекте age это строка "41". TypeScript всё равно считает его number, потому что any можно присвоить чему угодно, и программа печатает 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, reviver, вызывается для каждого ключа и может их восстановить:

Параметр value у reviver имеет тип any, как и результат, поэтому тип Order по-прежнему принимается на веру, а не проверяется. JSON.stringify(order, null, 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
bigintбросает TypeError

Правила времени выполнения те же, что и в обычном JavaScript; они описаны на странице JSON в JavaScript.

Тип для любого значения JSON

Когда код обрабатывает произвольный JSON, рекурсивный тип точно описывает, что может содержать JSON, и отвергает то, чего не может:

Без комментария последняя строка дала бы ошибку компиляции, потому что объект Date не является JsonValue.

Импорт файлов .json

Файл .json можно импортировать как модуль, и 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 импорт падает с ошибкой TS2732, Cannot find module './config.json'. Consider using '--resolveJsonModule' to import module with '.json' extension., пока вы не добавите "resolveJsonModule": true; значение false отключает импорт JSON везде. В ES-модуле при nodenext или node20 импорту нужен атрибут with { type: "json" } (без него ошибка TS1543), и разрешён только импорт по умолчанию (для import { port } ошибка TS1544). tsc копирует импортированный файл .json в outDir рядом со скомпилированным JavaScript.

Часто задаваемые вопросы

Какой тип возвращает JSON.parse в TypeScript?

any. Компилятор не может знать, что содержится в строке, поэтому const user: User = JSON.parse(text) компилируется, что бы ни было в тексте. Если данные приходят извне программы, присваивайте результат переменной типа unknown и проверяйте его.

Как преобразовать JSON в интерфейс TypeScript?

Возьмите показательный пример и напишите по одному свойству на ключ: строки, числа и булевы значения превращаются в string, number и boolean, вложенный объект становится отдельным интерфейсом, массив объектов превращается в Item[], а ключи, которые иногда отсутствуют, получают ?. Генераторы кода вроде quicktype делают это автоматически, но проверяйте их догадки больше чем на одном примере.

Как импортировать файл JSON в TypeScript?

import config from "./config.json"; работает в TypeScript 7, если module равен nodenext, node20, commonjs, esnext или preserve, а тип результата выводится из содержимого файла. С node16 или node18 задайте ещё "resolveJsonModule": true. В ES-модуле при nodenext или node20 добавьте атрибут, которого требует 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

НАЧАТЬ