For anyone with the same problem as me, what solved it was to do it the other way around. Instead of modifying Express.User to extend my custom IUser interface, I defined IUser as an extension of Express.User:
// user.interface.ts
interface IUser extends Express.User {
_id: string;
...
}
I then used type assertion when needed:
// index.ts
const user = req.user as IUser;
Is not elegant, and I still think there must be a way to do it the other way around so we don't have to do type assertion every time, but at least it's working now.