Переопределение типа свойства интерфейса, объявленного в .d.ts файле TypeScript
Проблема: расширение интерфейса не переопределяет существующие свойства
Когда вы создаёте интерфейс MyRequest extends Request, TypeScript не позволяет переопределить тип свойства user — он может только сузить его (сделать более специфичным), но не изменить на совершенно другой тип. В вашем случае any — это несовместимый тип с { id: string; name: string; role: 'admin' | 'user' }, поэтому компилятор выдаёт ошибку.
Правильное решение: использование декларативного слияния (declaration merging)
TypeScript поддерживает механизм, при котором несколько объявлений одного и того же интерфейса автоматически объединяются. Вы можете «дополнить» оригинальный интерфейс, объявив его повторно в своём файле с нужными изменениями. Однако есть нюанс: вы не можете переопределить существующее свойство — вы можете только добавить новые или изменить тип через условные типы.
Для вашей задачи лучше всего подойти через модульное расширение (module augmentation):
// types/express.d.ts
import 'express'; // Импортируем модуль, чтобы TypeScript знал о его существовании
declare module 'express' {
interface Request {
user: { id: string; name: string; role: 'admin' | 'user' };
}
}
Этот код говорит TypeScript: «В модуле express есть интерфейс Request, и я хочу добавить/изменить свойство user». Благодаря декларативному слиянию, ваше объявление объединится с оригинальным, и TypeScript заменит тип any на ваш кастомный тип.
Важные моменты:
- Файл должен быть модулем. Убедитесь, что ваш файл
types/express.d.tsсодержит хотя бы один импорт или экспорт, иначе TypeScript будет считать его глобальным скриптом, а не модулем, и объединение не сработает. - Порядок загрузки. Убедитесь, что ваш файл типов подключается после оригинального. Обычно достаточно поместить его в папку, указанную в
tsconfig.jsonв полеincludeилиtypeRoots. - Проверка совместимости. Если оригинальное свойство
userимеет типany, вы можете переопределить его любым типом. Если же тип был, например,string | undefined, то вы можете только сузить его (например, доstring).
Полный пример настройки tsconfig.json:
{
"compilerOptions": {
"typeRoots": ["./node_modules/@types", "./types"]
},
"include": ["src/**/*", "types/**/*"]
}
После этого в любом файле вашего проекта вы сможете писать:
import { Request } from 'express';
app.get('/profile', (req: Request, res: Response) => {
const userId = req.user.id; // TypeScript знает, что user — это объект с id, name, role
});
Этот подход работает для любой библиотеки с декларациями типов, будь то express, mongoose, socket.io и другие.
Альтернативный подход: использование утилитарных типов и переопределение через Omit и Pick
Если по какой-то причине модульное расширение не подходит (например, вы работаете с глобальными типами, а не модулями), можно создать новый тип на основе оригинального, исключив проблемное свойство и добавив его заново:
import { Request as ExpressRequest } from 'express';
type MyCustomUser = { id: string; name: string; role: 'admin' | 'user' };
// Создаём новый тип, исключая user из оригинального Request
type MyRequest = Omit<ExpressRequest, 'user'> & { user: MyCustomUser };
Теперь используйте MyRequest вместо Request в своих функциях. Этот метод не изменяет глобальный тип, а создаёт новый, что может быть полезно, если вы не хотите влиять на другие части проекта.
Недостаток: вам придётся везде использовать MyRequest вместо оригинального Request, что менее удобно, чем глобальное переопределение через модульное расширение.