Skip to content

TypeScript

ideal-auth is written in TypeScript and exports generic types that flow through the entire auth lifecycle. When you provide a user type to createAuth, every method that returns or accepts a user is typed accordingly.

The createAuth function accepts a generic type parameter TUser that must extend AnyUser:

type AnyUser = { id: string | number; [key: string]: any };

This means your user type needs at least an id field that is a string or number. Everything else is up to you.

import { createAuth } from 'ideal-auth';
interface User {
id: string;
email: string;
name: string;
role: 'admin' | 'user';
password: string;
}
const auth = createAuth<User>({
secret: process.env.SESSION_SECRET,
cookie: cookieBridge,
resolveUser: (id) => db.user.findUnique({ where: { id } }),
resolveUserByCredentials: (creds) =>
db.user.findUnique({ where: { email: creds.email } }),
hash,
});
const session = auth();
const user = await session.user(); // User | null
if (user) {
user.role; // 'admin' | 'user' — fully typed
user.email; // string
user.name; // string
}

The generic flows through every method on the auth instance:

  • session.user() returns Promise<User | null>
  • session.login(user) expects a User
  • session.attempt(creds) resolves the user through your resolveUserByCredentials, which must return Promise<User | null>

check() returns Promise<boolean> and user() returns Promise<TUser | null>. They are separate calls because check() is a lightweight session-validity check, while user() calls resolveUser to fetch the full user from your database.

The recommended pattern is to call user() and null-check the result:

const session = auth();
const user = await session.user();
if (!user) {
return res.status(401).json({ error: 'Not authenticated' });
}
// TypeScript narrows `user` to `User` here
console.log(user.role);

If you only need to know whether the user is authenticated without fetching user data, use check():

const session = auth();
if (!(await session.check())) {
return res.status(401).json({ error: 'Not authenticated' });
}
// If you also need the user, call user() separately
const user = await session.user(); // still User | null — check() doesn't narrow this

Typing resolveUser and resolveUserByCredentials

Section titled “Typing resolveUser and resolveUserByCredentials”

Both resolveUser and resolveUserByCredentials must return Promise<TUser | null> matching your generic. TypeScript enforces this at the config level.

import { createAuth, createHash } from 'ideal-auth';
import { db } from './db';
interface User {
id: string;
email: string;
name: string;
role: string;
password: string;
}
const hash = createHash();
const auth = createAuth<User>({
secret: process.env.SESSION_SECRET,
cookie: cookieBridge,
hash,
resolveUser: (id) => db.user.findUnique({ where: { id } }),
resolveUserByCredentials: (creds) =>
db.user.findUnique({ where: { email: creds.email } }),
});
import { eq } from 'drizzle-orm';
import { createAuth, createHash } from 'ideal-auth';
import { db } from './db';
import { users } from './schema';
interface User {
id: string;
email: string;
name: string;
password: string;
}
const hash = createHash();
const auth = createAuth<User>({
secret: process.env.SESSION_SECRET,
cookie: cookieBridge,
hash,
resolveUser: async (id) => {
const rows = await db.select().from(users).where(eq(users.id, id));
return (rows[0] as User) ?? null;
},
resolveUserByCredentials: async (creds) => {
const rows = await db
.select()
.from(users)
.where(eq(users.email, creds.email));
return (rows[0] as User) ?? null;
},
});
import { createAuth, createHash } from 'ideal-auth';
import { pool } from './db';
interface User {
id: string;
email: string;
name: string;
password: string;
}
const hash = createHash();
const auth = createAuth<User>({
secret: process.env.SESSION_SECRET,
cookie: cookieBridge,
hash,
resolveUser: async (id) => {
const { rows } = await pool.query<User>(
'SELECT * FROM users WHERE id = $1',
[id],
);
return rows[0] ?? null;
},
resolveUserByCredentials: async (creds) => {
const { rows } = await pool.query<User>(
'SELECT * FROM users WHERE email = $1',
[creds.email],
);
return rows[0] ?? null;
},
});

A common pattern is that your database model includes a password field, but you want the User type returned by session.user() to exclude it. There are two approaches.

Define a DBUser type that includes the password and a User type that omits it. Use DBUser internally and User as the generic:

interface DBUser {
id: string;
email: string;
name: string;
password: string;
}
type User = Omit<DBUser, 'password'>;
const auth = createAuth<User>({
secret: process.env.SESSION_SECRET,
cookie: cookieBridge,
hash,
resolveUser: async (id) => {
const row = await db.user.findUnique({ where: { id } });
if (!row) return null;
const { password, ...user } = row;
return user;
},
resolveUserByCredentials: async (creds) => {
// This function MUST return the password field for attempt() to work.
// ideal-auth reads it via passwordField before stripping it from the session.
return db.user.findUnique({ where: { email: creds.email } }) as any;
},
passwordField: 'password', // ideal-auth reads this field from the resolved user
});

Approach 2: Keep password in the type, strip in resolveUser

Section titled “Approach 2: Keep password in the type, strip in resolveUser”

A simpler approach is to include password in your type and strip it only in resolveUser (which is what session.user() calls):

interface User {
id: string;
email: string;
name: string;
password: string;
}
const auth = createAuth<User>({
secret: process.env.SESSION_SECRET,
cookie: cookieBridge,
hash,
resolveUser: async (id) => {
const user = await db.user.findUnique({
where: { id },
select: { id: true, email: true, name: true }, // omit password from select
});
return user as User | null;
},
resolveUserByCredentials: (creds) =>
db.user.findUnique({ where: { email: creds.email } }),
});

This way, resolveUserByCredentials returns the full object (including the password hash for attempt()), while resolveUser returns only the fields you want in the session.

ideal-auth exports all of its types so you can use them in your application code. Import them with the type keyword:

import type { AuthConfig, AuthInstance, HashInstance } from 'ideal-auth';

Here is the full list:

TypeDescription
AnyUserBase user constraint: { id: string | number; [key: string]: any }
CookieBridgeInterface for the cookie adapter: get, set, delete
CookieOptionsFull cookie options: httpOnly, secure, sameSite, path, maxAge, expires, domain
ConfigurableCookieOptionsOmit<CookieOptions, 'httpOnly'> — options you can override in config
SessionPayloadJWT-like payload stored in the cookie: { uid, iat, exp }
AuthConfig<TUser>Configuration object for createAuth
LoginOptionsOptions for login() and attempt(): { remember?: boolean }
AuthInstance<TUser>The session object returned by auth()
HashConfigConfiguration for createHash: { rounds?: number }
HashInstanceHash object with make(password) and verify(password, hash)
TokenVerifierConfigConfiguration for createTokenVerifier: { secret, expiryMs? }
TokenVerifierInstanceToken verifier with createToken(userId) and verifyToken(token)
RateLimitStoreInterface for custom rate limit stores: increment and reset
RateLimiterConfigConfiguration for createRateLimiter: { maxAttempts, windowMs, store? }
RateLimitResultResult of attempt(): { allowed, remaining, resetAt }
TOTPConfigConfiguration for createTOTP: { digits?, period?, window? }
TOTPInstanceTOTP object with generateSecret, generateQrUri, verify
RecoveryCodeResultResult of verifyRecoveryCode: { valid, remaining }

ConfigurableCookieOptions is defined as:

type ConfigurableCookieOptions = Omit<CookieOptions, 'httpOnly'>;

The httpOnly flag is excluded because ideal-auth always forces httpOnly: true on session cookies. This prevents client-side JavaScript from reading the session cookie, which is a critical XSS protection.

Use ConfigurableCookieOptions when typing session cookie overrides in your auth config:

import type { ConfigurableCookieOptions } from 'ideal-auth';
const cookieOverrides: Partial<ConfigurableCookieOptions> = {
secure: true,
sameSite: 'strict',
domain: '.example.com',
};
const auth = createAuth<User>({
secret: process.env.SESSION_SECRET,
cookie: cookieBridge,
session: {
cookie: cookieOverrides,
},
resolveUser: (id) => db.user.findUnique({ where: { id } }),
});

You can also use it when building wrappers or utility functions that accept cookie options:

import type { ConfigurableCookieOptions } from 'ideal-auth';
function createAuthWithDefaults(
cookieOptions?: Partial<ConfigurableCookieOptions>,
) {
return createAuth<User>({
secret: process.env.SESSION_SECRET,
cookie: cookieBridge,
session: {
cookie: {
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
...cookieOptions,
},
},
resolveUser: (id) => db.user.findUnique({ where: { id } }),
});
}