Type Safety
Leverage TypeScript for bulletproof analytics tracking
One of the biggest advantages of @stacksee/analytics is full TypeScript support. Define your events once and get autocomplete, type checking, and compile-time validation everywhere.
Why Type Safety Matters
Without type safety, analytics tracking is error-prone:
// ❌ Without type safety
analytics.track('user_signedup', { // Typo in event name!
emai: 'user@example.com', // Typo in property!
plan: 'premium' // Wrong value! (should be 'pro')
});
// Event is sent with wrong data
// You discover the error weeks later in your analytics dashboardWith type safety, these errors are caught at compile time:
// ✅ With type safety
analytics.track('user_signed_up', {
email: 'user@example.com',
plan: 'pro'
});
// TypeScript errors:
// - "user_signedup" doesn't exist, did you mean "user_signed_up"?
// - Property "emai" doesn't exist, did you mean "email"?
// - Type "premium" is not assignable to type "free" | "pro" | "enterprise"Event Type Definitions
The foundation of type safety is the event definition:
import type { CreateEventDefinition, EventCollection } from '@stacksee/analytics';
export const appEvents = {
userSignedUp: {
name: 'user_signed_up',
category: 'user',
properties: {} as {
email: string;
plan: 'free' | 'pro' | 'enterprise';
referralSource?: string;
}
}
} as const satisfies EventCollection<Record<string, CreateEventDefinition<string>>>;Breaking Down the Type Magic
Let's understand each part:
1. as const
Tells TypeScript to infer literal types instead of widening:
// Without as const
const event = {
name: 'user_signed_up' // Type: string
};
// With as const
const event = {
name: 'user_signed_up' // Type: 'user_signed_up'
} as const;2. satisfies EventCollection<...>
Validates the structure without widening types:
// Validates structure AND preserves literal types
export const appEvents = {
userSignedUp: { /* ... */ }
} as const satisfies EventCollection<Record<string, CreateEventDefinition<string>>>;
// TypeScript checks:
// ✅ Is this a valid EventCollection? Yes
// ✅ Preserve exact types? Yes3. properties: {} as { ... }
Defines the shape of event properties:
properties: {} as {
email: string; // Required property
plan: 'free' | 'pro'; // Union type for specific values
referralSource?: string; // Optional property
}Autocomplete Everywhere
Once events are typed, you get autocomplete in your IDE:
Event Names
analytics.track('user_')
// ↑
// IDE suggests:
// - user_signed_up
// - user_logged_in
// - user_updated_profileEvent Properties
analytics.track('user_signed_up', {
email: 'user@example.com',
pl
// ↑ IDE suggests: plan
})Property Values
analytics.track('user_signed_up', {
email: 'user@example.com',
plan: 'pr'
// ↑ IDE suggests: 'pro'
})Type Inference
TypeScript infers the correct property type for each event:
// TypeScript knows 'user_signed_up' requires these properties
analytics.track('user_signed_up', {
email: '', // ✅ Required
plan: 'pro', // ✅ Required
// TypeScript error: Property 'email' is missing
});
// TypeScript knows 'button_clicked' requires different properties
analytics.track('button_clicked', {
buttonId: '', // ✅ Required
location: '' // ✅ Required
});Generic Types
Pass event types to the analytics instance for full type safety:
import type { AppEvents } from './events';
// Client-side
const analytics = createClientAnalytics<AppEvents>({
providers: [/* ... */]
});
// Server-side
const serverAnalytics = createServerAnalytics<AppEvents>({
providers: [/* ... */]
});Now all methods are fully typed:
analytics.track(
// ↑ Autocomplete for event names
'user_signed_up',
// ↑ Autocomplete for properties
{ email: '', plan: 'pro' }
);User Traits Type Safety
Define types for user traits:
interface UserTraits {
email: string;
name: string;
plan: 'free' | 'pro' | 'enterprise';
company?: string;
role?: 'admin' | 'user' | 'viewer';
}
const analytics = createClientAnalytics<AppEvents, UserTraits>({
providers: [/* ... */]
});
// Now identify() is fully typed
analytics.identify('user-123', {
email: 'user@example.com',
name: 'John Doe',
plan: 'pro', // ✅ Autocomplete works!
role: 'admin'
// wrongProperty: true // ❌ TypeScript error!
});Type-Safe Event Categories
Use predefined or custom categories with type safety:
import type { EventCategory } from '@stacksee/analytics';
// Predefined categories (fully typed)
const category: EventCategory = 'user';
// ↑ Autocomplete suggests:
// 'product' | 'user' | 'navigation' | 'conversion' | 'engagement' | 'error' | 'performance'
// Custom categories (type-safe)
export const appEvents = {
aiGenerated: {
name: 'ai_generated',
category: 'ai' as const, // Custom category
properties: {} as {
model: string;
}
}
} as const;Complex Property Types
Define complex property structures with full type safety:
Nested Objects
export const appEvents = {
purchaseCompleted: {
name: 'purchase_completed',
category: 'conversion',
properties: {} as {
orderId: string;
total: number;
currency: 'USD' | 'EUR' | 'GBP';
items: Array<{
productId: string;
name: string;
price: number;
quantity: number;
}>;
shippingAddress: {
street: string;
city: string;
country: string;
};
}
}
} as const;Arrays
export const appEvents = {
productsViewed: {
name: 'products_viewed',
category: 'product',
properties: {} as {
productIds: string[];
categories: Array<'electronics' | 'clothing' | 'food'>;
}
}
} as const;Unions and Discriminated Unions
export const appEvents = {
paymentProcessed: {
name: 'payment_processed',
category: 'conversion',
properties: {} as {
method: 'card' | 'paypal' | 'crypto';
amount: number;
// Different data based on payment method
cardDetails?: {
last4: string;
brand: string;
};
paypalEmail?: string;
cryptoAddress?: string;
}
}
} as const;Type Guards and Validation
While TypeScript provides compile-time safety, you may want runtime validation:
import { z } from 'zod';
// Define Zod schema
const userSignedUpSchema = z.object({
email: z.string().email(),
plan: z.enum(['free', 'pro', 'enterprise']),
referralSource: z.string().optional()
});
// Use with analytics
function trackSignup(data: unknown) {
// Validate at runtime
const validated = userSignedUpSchema.parse(data);
// Now fully type-safe
analytics.track('user_signed_up', validated);
}Extracting Types
Extract types from your event definitions for reuse:
export const appEvents = {
userSignedUp: {
name: 'user_signed_up',
properties: {} as {
email: string;
plan: 'free' | 'pro' | 'enterprise';
}
}
} as const;
// Extract the properties type
type UserSignedUpProps = typeof appEvents.userSignedUp.properties;
// Type: { email: string; plan: 'free' | 'pro' | 'enterprise' }
// Use in functions
function validateSignup(data: UserSignedUpProps) {
analytics.track('user_signed_up', data);
}Conditional Types
Create type-safe helper functions:
type EventName = keyof typeof appEvents;
type EventProperties<T extends EventName> = typeof appEvents[T]['properties'];
// Type-safe tracking function
function trackEvent<T extends EventName>(
event: T,
properties: EventProperties<T>
) {
analytics.track(event, properties);
}
// Usage
trackEvent('user_signed_up', {
email: 'user@example.com',
plan: 'pro'
// TypeScript knows exactly what properties are required!
});Type-Safe Provider Configuration
Providers can also be type-safe:
import type { PostHogConfig } from '@stacksee/analytics/providers/client';
const config: PostHogConfig = {
token: 'xxx',
api_host: 'https://app.posthog.com',
debug: true
// TypeScript validates all options!
};
const analytics = createClientAnalytics({
providers: [new PostHogClientProvider(config)]
});Common Patterns
Shared Event Properties
Create base interfaces for common properties:
interface BaseEventProps {
timestamp?: number;
sessionId?: string;
}
export const appEvents = {
buttonClicked: {
name: 'button_clicked',
properties: {} as BaseEventProps & {
buttonId: string;
location: string;
}
}
} as const;Event Builder Functions
Create type-safe event builders:
export const appEvents = {
userSignedUp: {
name: 'user_signed_up',
properties: {} as {
email: string;
plan: 'free' | 'pro' | 'enterprise';
}
}
} as const;
// Type-safe builder
export function createUserSignedUpEvent(
email: string,
plan: 'free' | 'pro' | 'enterprise'
) {
return {
email,
plan,
timestamp: Date.now()
};
}
// Usage
analytics.track('user_signed_up', createUserSignedUpEvent(
'user@example.com',
'pro'
));Best Practices
1. Always Use as const satisfies
// ✅ Good - preserves literal types
export const appEvents = {
/* ... */
} as const satisfies EventCollection<Record<string, CreateEventDefinition<string>>>;
// ❌ Bad - loses type information
export const appEvents = {
/* ... */
};2. Define Specific Union Types
// ✅ Good - specific values
properties: {} as {
plan: 'free' | 'pro' | 'enterprise';
}
// ❌ Bad - too loose
properties: {} as {
plan: string;
}3. Mark Optional Properties
// ✅ Good - clearly optional
properties: {} as {
email: string;
referralSource?: string; // Optional with ?
}
// ❌ Bad - unclear if required
properties: {} as {
email: string;
referralSource: string | undefined;
}4. Export Event Types
// lib/events.ts
export const appEvents = { /* ... */ } as const;
export type AppEvents = typeof appEvents;
// Use elsewhere
import type { AppEvents } from '@/lib/events';