Как импортировать JSON в TypeScript и не сойти с ума
Если вы когда-нибудь пробовали сделать import data from './config.json' в TypeScript, то знаете это чувство: компилятор краснеет, ругается и выбрасывает ошибку Cannot find module. Знакомая боль? Ещё бы! Вопрос про импорт JSON в TypeScript — один из самых популярных на Stack Overflow. И это не удивительно: JSON используется повсюду — конфиги, локализация, мок-данные, статические списки. Но TypeScript по умолчанию не умеет работать с JSON-файлами.
В этом туториале мы разберём все способы импорта JSON в TypeScript — от простого включения флага в tsconfig до продвинутой типизации. Вы узнаете, как заставить TypeScript дружить с JSON, и больше никогда не увидите эту красную ошибку. Поехали!
Почему TypeScript не видит JSON?
Прежде чем нырять в код, давайте разберёмся в причине. TypeScript — это надстройка над JavaScript, которая добавляет статическую типизацию. По умолчанию компилятор TypeScript ожидает, что все импортируемые модули — это .ts или .js файлы. JSON — это не модуль, это формат данных. TypeScript просто не знает, как его интерпретировать.
Решение лежит в двух плоскостях:
- Настройка компилятора — включить поддержку JSON через флаг
resolveJsonModule. - Объявление модуля — вручную описать тип для JSON-файлов, если флаг не подходит.
Способ 1: Включаем resolveJsonModule (правильный путь)
Самый простой и рекомендуемый способ — добавить в tsconfig.json опцию resolveJsonModule. Это флаг, который говорит TypeScript: "Эй, дружище, давай будем импортировать JSON как обычные модули".
// tsconfig.json
{
"compilerOptions": {
"resolveJsonModule": true,
"esModuleInterop": true
}
}
После этого вы спокойно можете писать:
// config.ts
import config from './config.json';
console.log(config.apiUrl); // работает!
console.log(config.port); // работает!
Что здесь происходит? TypeScript автоматически создаёт типы для импортируемого JSON. Если в вашем JSON есть поле apiUrl: string и port: number, TypeScript выведет их типы и будет проверять использование. Круто, правда?
Но есть нюанс: resolveJsonModule также требует включения esModuleInterop или allowSyntheticDefaultImports, чтобы импорт по умолчанию работал корректно. Без этого вы получите ошибку при попытке импорта.
Способ 2: Импорт с именованными экспортами
Иногда вам нужен не весь JSON, а только определённые поля. TypeScript поддерживает и это:
import { apiUrl, port } from './config.json';
console.log(apiUrl); // строка
console.log(port); // число
Это возможно благодаря так называемому "tree-shaking" в сборщиках (Webpack, Vite), но на уровне TypeScript это просто деструктуризация объекта. Однако помните: если вы используете resolveJsonModule, TypeScript автоматически создаёт типы для всех полей, поэтому можно смело импортировать как весь объект, так и его части.
Способ 3: require вместо import (для старых проектов)
Если вы работаете в проекте, где resolveJsonModule по каким-то причинам включить нельзя (например, конфликт с другими настройками), можно использовать старый добрый require:
const config = require('./config.json');
console.log(config.apiUrl); // работает, но без типов
Проблема этого подхода очевидна: вы теряете типизацию. TypeScript не знает, что лежит внутри config, и будет считать его типом any. Это открывает дорогу ошибкам. Если уж используете require, хотя бы добавьте объявление типа:
interface Config {
apiUrl: string;
port: number;
}
const config: Config = require('./config.json');
console.log(config.apiUrl); // теперь с типом string
Способ 4: Создаём глобальное объявление модуля
Бывает, что вы не можете менять tsconfig.json (например, в библиотеке или monorepo). Тогда можно создать файл json.d.ts и написать в нём:
// json.d.ts
declare module "*.json" {
const value: any;
export default value;
}
Этот трюк говорит TypeScript: "Любой файл с расширением .json — это модуль, который экспортирует по умолчанию что-то (тип any)". Минус — вы снова теряете типизацию. Лучше сделать так:
// json.d.ts
declare module "*.json" {
const value: Record<string, unknown>;
export default value;
}
Хотя это всё равно не даст автодополнения для конкретных полей. Для полной типизации придётся либо генерировать типы динамически, либо использовать resolveJsonModule.
Типизация JSON: как сделать идеально
Допустим, у вас есть сложный JSON с вложенными объектами и массивами. resolveJsonModule выведет типы автоматически, но если вы хотите явно описать структуру, сделайте так:
// config.json
{
"apiUrl": "https://api.example.com",
"port": 8080,
"features": {
"darkMode": true,
"notifications": false
}
}
// config.ts
import config from './config.json';
interface Config {
apiUrl: string;
port: number;
features: {
darkMode: boolean;
notifications: boolean;
};
}
const typedConfig = config as Config;
console.log(typedConfig.features.darkMode); // true
Кастинг через as — это костыль, но он работает. Более элегантный способ — использовать satisfies (TypeScript 4.9+):
const typedConfig = config satisfies Config;
Это не меняет тип, но проверяет, что объект соответствует интерфейсу. Если в JSON появится лишнее поле — ошибки не будет, если не хватает обязательного — будет.
Реальный пример: конфиг приложения
Давайте соберём всё вместе. Представьте, что у вас есть файл app.config.json:
{
"appName": "MySuperApp",
"version": "2.0.1",
"database": {
"host": "localhost",
"port": 5432,
"user": "admin",
"password": "secret"
},
"logging": {
"level": "info",
"file": "/var/log/app.log"
}
}
Импортируем его в main.ts с полной типизацией:
import appConfig from './app.config.json';
interface DatabaseConfig {
host: string;
port: number;
user: string;
password: string;
}
interface LoggingConfig {
level: string;
file: string;
}
interface AppConfig