0

I’m using a Turborepo monorepo with a Next.js 15 App Router project (apps/web) and a shared packages/types package that contains my NextAuth module augmentation.

However, TypeScript still gives this error:

Property 'id' does not exist on type '{ name?: string | null; email?: string | null; image?: string | null }'

This happens in my NextAuth route (apps/web/src/app/api/auth/[...nextAuth]/route.ts):

if (session.user) {
  session.user.id = token.sub!;
}

Folder structure

my-turborepo/
│
├── tsconfig.json
│
├── apps/
│   └── web/
│       ├── package.json
│       ├── tsconfig.json
│       └── src/app/api/auth/[...nextAuth]/route.ts
│
└── packages/
    └── types/
        ├── package.json
        ├── tsconfig.json
        └── next-auth.d.ts

📄 packages/types/next-auth.d.ts

import { DefaultSession } from "next-auth";

declare module "next-auth" {
  interface Session {
    user: {
      id: string;
    } & DefaultSession["user"];
  }

  interface User {
    id: string;
  }
}

declare module "next-auth/jwt" {
  interface JWT {
    id: string;
  }
}

export {};

📄 packages/types/tsconfig.json

{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "emitDeclarationOnly": true
  },
  "include": ["./**/*.d.ts"]
}

📄 packages/types/package.json

{
  "name": "@repo/types",
  "version": "1.0.0",
  "types": "./next-auth.d.ts"
}

📄 apps/web/package.json

"dependencies": {
  "@repo/types": "workspace:*",
  "@repo/db": "workspace:*",
  "next": "15.5.6",
  "next-auth": "^4.24.13",
  "react": "19.1.0",
  "react-dom": "19.1.0"
}

📄 apps/web/tsconfig.json

{
  "compilerOptions": {
    "target": "ES2017",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [{ "name": "next" }],
    "paths": {
      "@/*": ["./src/*"]
    },
    "typeRoots": [
      "../../packages/types",
      "./node_modules/@types"
    ]
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts",
    "../../packages/types/**/*.d.ts"
  ],
  "exclude": ["node_modules"]
}

📄 root/tsconfig.json

{
  "files": [],
  "references": [
    { "path": "apps/web" },
    { "path": "packages/types" }
  ],
  "compilerOptions": {
    "composite": true
  }
}

The actual NextAuth route

apps/web/src/app/api/auth/[...nextAuth]/route.ts

import NextAuth, { NextAuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";

export const authOptions: NextAuthOptions = {
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
    })
  ],
  callbacks: {
    async session({ session, token }) {
      if (session.user) {
        session.user.id = token.sub!;
      }
      return session;
    }
  }
};

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };

Despite everything, TypeScript still does not merge the module augmentation, and I still get:

Property 'id' does not exist on type 'Session["user"]'

Question

What am I missing?
How can I get NextAuth module augmentation to work in a TurboRepo monorepo with a shared packages/types folder?

1 Answer 1

0

Before answering, let me clarify 2 things here:

  1. Your issue has nothing to do with the fact that you're using Turborepo (or any monorepo).
  2. You don't have a Typescript issue either. You have a Javascript issue. Meaning the id property actually doesn't exist in session.user.id. By using type assertion (e.g. !) you're negating all the benefits of Typecsript. I recommend performing simple if checks instead, which will make your code more robust and make Typescript compiler happy.

Now, for the answer, Next Auth works in a way that you need to "catch" the user data right after the registration and before redirect. During that small window, it returns everything in the jwt() callback. You can then grab it and modify the token data by appending the data you need. Here's an example:

async jwt({ token, user, session, trigger }) {
  if (user && user.id) {
    token.id = user.id // <--- we grab the user ID and append it to our token
  }

  // The code below is from a project of mine and doesn't apply to your case.
  // I just wanted to show you how I'm using `jwt()` callback to populate the `token` object in case it's useful.
  if (trigger === 'update') {
    if (session.user.name && session.user.name !== token.name) {
      token.name = session.user.name
    }

    if (session.user.roomId && session.user.roomId !== token.roomId) {
      token.roomId = session.user.roomId
    }
  }

  return token
},

Now that our token has an additional id property, we can access it from the session() callback like so:

session({ session, token }) {
  if (token.id) {
    session.user.id = token.id
  }

  // Once again, the part below is irrelevant, just wanted to show you how I had it in my project.
  if (token.name) {
    session.user.name = token.name
  }

  if (token.roomId) {
    session.user.roomId = token.roomId
  }

  return session
},

So basically, your issue is that you're trying to access the token property in your session() callback, but you never actually populated it.

I also recommend logging out everything you receive in your jwt() and session() so you have a better idea of the data (your jwt() logs will apear in terminal because it's server side).

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.