TypeScript
The Volcano SDK includes comprehensive TypeScript definitions for type-safe development. This guide covers type usage, generics, and best practices.
The Volcano SDK includes comprehensive TypeScript definitions for type-safe development. This guide covers type usage, generics, and best practices.
Installation
TypeScript definitions are included with the SDK - no additional packages needed:
npm install @volcano.dev/sdkBasic Usage
Import types alongside the SDK:
import { VolcanoAuth } from '@volcano.dev/sdk';
import type { User, Session, AuthResponse } from '@volcano.dev/sdk';
const volcano = new VolcanoAuth({
apiUrl: process.env.VOLCANO_API_URL!,
anonKey: process.env.VOLCANO_ANON_KEY!,
});
// Types are inferred automatically
const { user, session, error } = await volcano.auth.signIn({
email: 'user@example.com',
password: 'password123',
});
if (user) {
console.log(user.email); // TypeScript knows this is a string
}Core Types
Configuration
interface VolcanoAuthConfig {
/** API URL (defaults to https://api.volcano.dev) */
apiUrl?: string;
/** Project anon key (required) */
anonKey: string;
/** Access token for server-side use */
accessToken?: string;
/** Refresh token for server-side use */
refreshToken?: string;
}User
interface User {
id: string;
email: string;
user_metadata?: Record<string, JsonValue>;
created_at: string;
updated_at: string;
}Session
interface Session {
access_token: string;
refresh_token: string;
expires_in: number;
}Response Types
interface AuthResponse {
user: User | null;
session: Session | null;
error: Error | null;
}
interface UserResponse {
user: User | null;
error: Error | null;
}
interface SessionResponse {
session: Session | null;
error: Error | null;
}Database Queries
Typed Query Results
Use generics for type-safe query results:
interface Post {
id: string;
title: string;
content: string;
status: 'draft' | 'published' | 'archived';
user_id: string;
created_at: string;
}
// Query returns Post[]
const { data, error } = await volcano.from<Post>('posts').select('*').eq('status', 'published');
if (data) {
data.forEach((post) => {
console.log(post.title); // TypeScript knows this is string
console.log(post.status); // TypeScript knows this is 'draft' | 'published' | 'archived'
});
}Insert with Types
// TypeScript validates the insert values
const { data, error } = await volcano.insert<Post>('posts', {
title: 'New Post',
content: 'Content here',
status: 'draft',
// user_id is likely auto-set by RLS
});Update with Types
const { data, error } = await volcano
.update<Post>('posts', {
status: 'published', // TypeScript ensures this is a valid status
})
.eq('id', postId);Query Builder Types
interface QueryBuilder<T> {
select(columns: string): QueryBuilder<T>;
eq(column: string, value: FilterValue): QueryBuilder<T>;
neq(column: string, value: FilterValue): QueryBuilder<T>;
gt(column: string, value: FilterValue): QueryBuilder<T>;
gte(column: string, value: FilterValue): QueryBuilder<T>;
lt(column: string, value: FilterValue): QueryBuilder<T>;
lte(column: string, value: FilterValue): QueryBuilder<T>;
like(column: string, pattern: string): QueryBuilder<T>;
ilike(column: string, pattern: string): QueryBuilder<T>;
is(column: string, value: null): QueryBuilder<T>;
in(column: string, values: FilterValue[]): QueryBuilder<T>;
order(column: string, options?: { ascending?: boolean }): QueryBuilder<T>;
limit(count: number): QueryBuilder<T>;
offset(count: number): QueryBuilder<T>;
execute(): Promise<QueryResult<T>>;
}
interface QueryResult<T> {
data: T[] | null;
error: Error | null;
count?: number;
}Storage Types
interface StorageObject {
id: string;
bucket_id: string;
name: string;
owner_id?: string;
is_public: boolean;
size: number;
mime_type: string;
etag?: string;
metadata?: Record<string, JsonValue>;
created_at: string;
updated_at: string;
public_url?: string;
}
interface StorageUploadOptions {
contentType?: string;
}
interface StorageUploadResponse {
data: StorageObject | null;
error: Error | null;
}
interface StorageDownloadResponse {
data: Blob | null;
error: Error | null;
}
interface StorageListResponse {
data: StorageObject[] | null;
error: Error | null;
nextCursor: string | null;
}Usage
const storage = volcano.storage.from('avatars');
// Upload
const { data, error }: StorageUploadResponse = await storage.upload('user/avatar.jpg', file, {
contentType: 'image/jpeg',
});
// Download
const { data: blob }: StorageDownloadResponse = await storage.download('user/avatar.jpg');
// List
const { data: files, nextCursor }: StorageListResponse = await storage.list('user/', {
limit: 100,
});Realtime Types
Import realtime types from the realtime module:
import { VolcanoRealtime, RealtimeChannel } from '@volcano.dev/sdk/realtime';
import type {
PostgresChange,
PresenceState,
ConnectContext,
DisconnectContext,
ErrorContext,
RealtimeConfig,
ChannelOptions,
WebSocketConstructor,
} from '@volcano.dev/sdk/realtime';Realtime Configuration
interface RealtimeConfig {
apiUrl: string;
anonKey: string;
accessToken?: string;
getToken?: () => Promise<string>;
volcanoClient?: VolcanoAuth;
fetchConfig?: FetchConfig;
webSocket?: WebSocketConstructor;
}
interface ChannelOptions {
type?: 'broadcast' | 'presence' | 'postgres';
autoFetch?: boolean;
fetchBatchWindowMs?: number;
fetchMaxBatchSize?: number;
}Event Contexts
interface ConnectContext {
client?: string;
latency?: number;
}
interface DisconnectContext {
code?: number;
reason?: string;
reconnect?: boolean;
}
interface ErrorContext {
error?: Error;
message?: string;
code?: number;
}Postgres Changes
interface PostgresChange {
table: string;
schema: string;
type: 'INSERT' | 'UPDATE' | 'DELETE';
record?: Record<string, unknown>;
old_record?: Record<string, unknown>;
columns?: string[];
timestamp: string;
}Typed Postgres Listeners
interface Message {
id: string;
content: string;
user_id: string;
created_at: string;
}
channel.onPostgresChanges('INSERT', 'public', 'messages', (change: PostgresChange) => {
const message = change.record as Message;
console.log('New message:', message.content);
});Presence State
interface PresenceState {
[clientId: string]: Record<string, unknown>;
}
// Type your presence data
interface UserPresence {
user_id: string;
username: string;
status: 'online' | 'away' | 'busy';
}
channel.onPresenceSync((state: PresenceState) => {
const users = Object.values(state) as UserPresence[];
users.forEach((user) => {
console.log(`${user.username} is ${user.status}`);
});
});Functions Types
Typed Function Invocation
interface InvokeParams {
userId: string;
action: 'fetch' | 'update' | 'delete';
}
interface InvokeResult {
success: boolean;
data?: Record<string, unknown>;
message?: string;
}
const { data, error } = await volcano.functions.invoke<InvokeParams, InvokeResult>('process-user', {
userId: '123',
action: 'fetch',
});
if (data) {
console.log(data.success); // TypeScript knows this is boolean
console.log(data.message); // TypeScript knows this is string | undefined
}A platform-layer invocation failure is a VolcanoSystemError (exported from
@volcano.dev/sdk). Narrow with the VolcanoSystemError.is() type guard — it
gives full type narrowing without a cast and holds across duplicate SDK copies
in a bundle:
import { VolcanoSystemError } from '@volcano.dev/sdk';
if (error && VolcanoSystemError.is(error)) {
// deploy down/failed, gateway error, or network failure — infra, retryable
console.error(error.status, error.message); // `status` is typed here
}OAuth Types
type OAuthProviderName = 'google' | 'github' | 'microsoft' | 'apple';
interface OAuthProvider {
provider: OAuthProviderName;
linked_at: string;
updated_at: string;
}
interface OAuthAPIParams {
endpoint: string;
method?: string;
body?: JsonValue;
}Usage
// TypeScript ensures valid provider names
volcano.auth.signInWithOAuth('google'); // OK
volcano.auth.signInWithOAuth('invalid'); // Error: not assignable to 'OAuthProviderName'
// Get linked providers
const { providers } = await volcano.auth.getLinkedOAuthProviders();
if (providers) {
providers.forEach((p: OAuthProvider) => {
console.log(`${p.provider} linked at ${p.linked_at}`);
});
}Session Management Types
interface AuthSession {
id: string;
user_id: string;
provider: 'email' | 'google' | 'github' | 'microsoft' | 'apple' | 'anonymous';
user_agent?: string;
ip_address?: string;
last_ip_address?: string;
expires_at: string;
last_activity_at?: string;
session_started_at?: string;
is_active: boolean;
is_current: boolean;
created_at: string;
updated_at: string;
}
interface SessionsResponse {
sessions: AuthSession[] | null;
total: number;
page: number;
limit: number;
total_pages: number;
error: Error | null;
}Middleware Types
import type {
ServerClientConfig,
ServerClient,
User,
GetUserResult,
RefreshTokenResult,
} from '@volcano.dev/sdk/next/middleware';
interface ServerClientConfig {
anonKey: string;
apiUrl?: string;
accessToken?: string;
}
interface ServerClient {
getUser(accessToken: string): Promise<GetUserResult>;
refreshToken(refreshToken: string): Promise<RefreshTokenResult>;
}
interface GetUserResult {
user: User | null;
error: Error | null;
}
interface RefreshTokenResult {
accessToken: string | null;
refreshToken: string | null;
error: Error | null;
}Utility Types
JsonValue
A recursive type for JSON-serializable values:
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };FilterValue
Values accepted by query filters:
type FilterValue = string | number | boolean | null | Date;UserMetadata
Type for user metadata:
type UserMetadata = Record<string, JsonValue>;
// Usage
await volcano.auth.signUp({
email: 'user@example.com',
password: 'password',
metadata: {
name: 'Alice',
preferences: {
theme: 'dark',
notifications: true,
},
} as UserMetadata,
});Best Practices
1. Define Your Data Models
Create interfaces for your database tables:
// types/database.ts
export interface Post {
id: string;
title: string;
content: string;
status: 'draft' | 'published' | 'archived';
user_id: string;
created_at: string;
updated_at: string;
}
export interface Comment {
id: string;
post_id: string;
user_id: string;
content: string;
created_at: string;
}
export interface User {
id: string;
email: string;
name: string;
avatar_url?: string;
}2. Use Type Guards
function isPost(data: unknown): data is Post {
return (
typeof data === 'object' &&
data !== null &&
'id' in data &&
'title' in data &&
'content' in data
);
}
const { data } = await volcano.from('posts').select('*');
if (data && data.every(isPost)) {
// data is typed as Post[]
}3. Handle Null Errors
Always check for errors and null data:
const { data, error } = await volcano.from<Post>('posts').select('*');
if (error) {
// Handle error
console.error(error.message);
return;
}
if (!data) {
// Handle empty result
return;
}
// data is guaranteed to be Post[]
data.forEach((post) => {
console.log(post.title);
});4. Type Your Callbacks
const unsubscribe = volcano.auth.onAuthStateChange((user: User | null) => {
if (user) {
// TypeScript knows user properties
console.log(user.email);
}
});5. Use Strict Mode
Enable strict TypeScript for best type checking:
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"strictNullChecks": true
}
}Next Steps
- Getting Started - Basic SDK usage
- Database - Query builder with types
- Realtime - Typed realtime subscriptions