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 |
|---|---|
Date | ISO-строка (через его метод toJSON) |
Map, Set | {} (сначала преобразуйте через [...set] или Object.fromEntries(map)) |
undefined, функции, символы в объекте | ключ пропускается |
undefined, функции, символы в массиве | null |
отдельно взятые undefined, функция или символ | undefined, а не строка |
NaN, Infinity | null |
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 превращаются в {}.