typescript - Hur man använder numeriskt användar-id med next-auth


Jag hittade nyligen och började testa create-t3-app som en ny bas för NextJS-projekt eftersom det tar hand om mycket av boilerplate-uppsättningen för saker som TypeScript, trpc, prisma och next-auth, så det skulle spara mig mycket tid. Även om det är relevant tror jag inte att det är källan till mitt problem. Mitt problem är att jag använder en MySQL-databas med autoinkrementerande användar-ID:n, men det verkar som att typerna för next-auth-paketet tvingar mig att använda en sträng (DefaultUser i next-auth/core/types.d.ts och AdapterUser i next-auth/adapters.d.ts sätter båda typen av id till sträng och kommentarerna säger UUIDs). Tanken att jag eventuellt skulle kunna utöka det som finns för att stödja numeriska användar-ID:n, lade jag till detta i 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'];
  }
}

Vilket verkar fungera på de flesta ställen förutom i [...nextauth].ts där det ger mig detta fel

Typen 'string | number' kan inte tilldelas typen 'number'. Typen 'string' kan inte tilldelas typen 'number'.ts(2322)

På raden session.user.id=user.id i den här delen av koden

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)

TypeScript-felet försvinner om jag tar bort raden id: string; från AdapterUser i next-auth/adapters.d.ts, vilket då faller tillbaka på id: number; eftersom jag satt det på User

export interface AdapterUser extends User {
    id: string; // <-- I removed this
    email: string;
    emailVerified: Date | null;
}

Jag tycker inte att jag borde behöva ändra bibliotekets typer för att stödja numeriska användar-ID:n, men jag har slut på idéer om hur jag ska lösa detta och har inte hittat några svar online. Eller bör jag inte använda numeriska ID:n även om min databas gör det? Om det hjälper, använder jag

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

Svar #1

Tyvärr verkar det inte vara möjligt att "laga" eller ändra typerna som biblioteket har - även att försöka ändra NextAuthOptions fungerar inte eftersom efterföljande egenskapdefinitioner måste ha samma typ. Under tiden kommer du att behöva använda en kontroll/omvandling vid körning eller göra en typomvandling:

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: []
}

Svar #2

Som en fortsättning på det ovan accepterade svaret använde jag runtime-typkontroller för att göra mitt liv lite enklare.

I mitt fall hade min Prisma-användare några extra attribut också:

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")
}

Så jag definierade dessa typvakter:

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;
};

Sedan, i sessionsåteruppringningen, använde jag en av typvaktarna på följande sätt:

session: async ({ session, user }) => {
  if (!isPrismaUser(user)) {
    return session;
  }
  const identifiedSession: IdentifiedSession = {
    ...session,
    user,
    id: user.id,
  };
  return identifiedSession;
}

Vilket innebär att signaturen på callbacken nu är Session | IdentifiedSession, vilket jag hanterar i nedströmskonsumenter på detta sätt

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 },
    };
};

Relaterade artiklar