Reputation: 1111
I recently found and started testing create-t3-app as a new base for NextJS projects because it takes care of a lot of boilerplate setup for things like TypeScript, trpc, prisma, and next-auth so it'd save me a ton of time. While that's relevant, I don't think it's the source of my problem. My problem is that I use a MySQL database with auto-incrementing user IDs but it seems like the types for the next-auth
package are forcing me to use a string (DefaultUser
in next-auth/core/types.d.ts
and AdapterUser
in next-auth/adapters.d.ts
both set the type of id to string and the comments say UUIDs). Thinking that I could possibly extend what's there to support numeric user IDs, I added this to next-auth.d.ts
:
import { DefaultSession, DefaultUser } from 'next-auth'
declare module 'next-auth' {
interface User extends DefaultUser {
id: number;
}
interface Session {
user?: {
id: number;
} & DefaultSession['user'];
}
}
Which seems to work most places except in [...nextauth].ts
where it is giving me this error
Type 'string | number' is not assignable to type 'number'. Type 'string' is not assignable to type 'number'.ts(2322)
On the session.user.id = user.id
line in this section of code
export const authOptions: NextAuthOptions = {
// Include user.id on session
callbacks: {
session({ session, user }) {
if (session.user) {
session.user.id = user.id
}
return session
}
},
adapter: PrismaAdapter(prisma),
providers: []
}
export default NextAuth(authOptions)
The TypeScript error goes away if I delete the id: string;
line from the AdapterUser
from next-auth/adapters.d.ts
which then falls back to id: number;
because I set that on User
export interface AdapterUser extends User {
id: string; // <-- I removed this
email: string;
emailVerified: Date | null;
}
I don't think I should have to modify the library's types to support numeric user IDs but I'm all out of ideas how to solve this and haven't found answers online. Or should I not be using numeric IDs even though my database does? If it helps, I'm using
next 12.3.1
next-auth 4.12.3
react 18.2.0
react-dom 18.2.0
superjson 1.9.1
zod 3.18.0
eslint 8.22.0
postcss 8.4.14
prettier 2.7.1
prisma 4.4.0
tailwindcss 3.1.6
typescrypt 4.8.0
Upvotes: 9
Views: 2431
Reputation: 191
Expanding on the above accepted answer, I utilized runtime typeguards to make my life a little easier.
In my case, my Prisma user had a few extra attributes as well:
model User {
id Int @id @default(autoincrement())
uuid String @unique @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
categories Category[]
sessions Session[]
records Record[]
permissionsGranted SharePermission[] @relation("permissions_granted")
permissionsReceived SharePermission[] @relation("permissions_received")
@@unique([id, uuid])
@@index(fields: [id, uuid], name: "user_by_id_idx")
}
So I defined these typeguards:
import { User as PrismaUser } from '@prisma/client';
import { Session, User } from 'next-auth';
export interface IdentifiedSession extends Session {
id: number;
user: PrismaUser;
}
export const isNextAuthUser = (value: unknown): value is User => {
const { id, name, email, image } = (value ?? {}) as User;
const idIsValid = typeof id === 'number';
const nameIsValid = typeof name === 'string' || !name;
const emailIsValid = typeof email === 'string' || !email;
const imageIsValid = typeof image === 'string' || !image;
return idIsValid && nameIsValid && emailIsValid && imageIsValid;
};
export const isPrismaUser = (value: unknown): value is PrismaUser => {
const { uuid, emailVerified } = (value ?? {}) as PrismaUser;
const uuidIsValid = typeof uuid === 'string';
const emailVerifiedIsEmail = emailVerified instanceof Date || !emailVerified;
return isNextAuthUser(value) && uuidIsValid && emailVerifiedIsEmail;
};
export const isSession = (value: unknown): value is Session => {
const { user, expires } = (value ?? {}) as Session;
const userIsValid = isPrismaUser(user) || isNextAuthUser(user) || !user;
const expiresIsValid = typeof expires === 'string';
return expiresIsValid && userIsValid;
};
export const isIdentifiedSession = (value: unknown): value is IdentifiedSession => {
const session = (value ?? {}) as IdentifiedSession;
const sessionIsValid = isSession(session);
const sessionUserIsValid = isPrismaUser(session.user);
return sessionIsValid && sessionUserIsValid;
};
Then, in the session callback, I utilized one of the typeguards like so:
session: async ({ session, user }) => {
if (!isPrismaUser(user)) {
return session;
}
const identifiedSession: IdentifiedSession = {
...session,
user,
id: user.id,
};
return identifiedSession;
}
Which does mean that the signature of the callback is now Session | IdentifiedSession
, which I handle in downstream consumers as such
export const getServerSideProps: GetServerSideProps = async ({ req, res }) => {
const session = await getSession({ req });
if (!isIdentifiedSession(session)) {
return {
props: {
records: [],
},
};
}
const records = await prisma.record.findMany({
where: {
reporter: {
id: session.id,
},
},
orderBy: {
date: 'asc',
},
include: {
category: {
select: {
title: true,
},
},
},
});
return {
props: { records },
};
};
Upvotes: 0
Reputation: 26327
Unfortunately it doesn't seem possible to "patch up" or change the types the library has - even trying to change NextAuthOptions
doesn't work since subsequent property definitions must have the same type. In the meantime, you'll have to use a runtime check/coercion or cast:
export const authOptions: NextAuthOptions = {
// Include user.id on session
callbacks: {
session({ session, user }) {
if (session.user) {
if (typeof user.id !== "number") throw new Error("id should a number");
session.user.id = user.id // OK
// session.user.id = +user.id // more dangerous but still works
// session.user.id = user.id as number // also dangerous
}
return session
}
},
adapter: PrismaAdapter(prisma),
providers: []
}
Upvotes: 3