Type Safety
Define your events once and get autocomplete, compile-time checks, and inferred property types across client and server.
trakoo is built around one typed event definition. Define your events once, pass the type to your analytics instance, and every track() and identify() call is checked against it. Misspelled event names, missing properties, and wrong values fail at compile time instead of silently shipping bad data.
Why type safety matters
Analytics bugs are quiet. A typo in an event name or a property doesn’t throw — it records the wrong thing, and you find out weeks later when a funnel comes up empty.
// Avoid: no types, so nothing catches these mistakes
analytics.track('user_signedup', { // wrong event name
emai: 'user@example.com', // wrong property
plan: 'premium' // not a valid plan
});
With a typed instance, each of those is a compile error before the event ships:
// Good: every argument is checked against your event definition
analytics.track('user_signed_up', {
email: 'user@example.com',
plan: 'pro'
});
// TypeScript flags:
// - 'user_signedup' is not a known event name
// - 'emai' does not exist on this event's properties
// - 'premium' is not assignable to 'free' | 'pro' | 'enterprise'
The event definition
Everything starts with one typed collection of events. The as const satisfies combination is what makes the checks work.
import type { CreateEventDefinition, EventCollection } from 'trakoo';
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>>>;
export type AppEvents = typeof appEvents;
Three pieces do the work.
as const
Without it, TypeScript widens each literal to its base type — name becomes string, and 'user' becomes string. as const locks every value to its literal type, so trakoo can offer 'user_signed_up' as an autocomplete suggestion instead of “any string”.
const a = { name: 'user_signed_up' }; // name: string
const b = { name: 'user_signed_up' } as const; // name: 'user_signed_up'
satisfies EventCollection<...>
satisfies checks that your object matches the expected shape without changing its inferred type. You get validation — a malformed event is a compile error — while keeping the exact literal types from as const.
properties: {} as { ... }
Event properties are a type, not a runtime value. {} as { ... } hands trakoo the property shape to check track() calls against, without allocating a real object at runtime.
properties: {} as {
email: string; // required
plan: 'free' | 'pro'; // union — only these values
referralSource?: string; // optional
}
Autocomplete everywhere
Pass your event type to the analytics instance and every call is driven by it.
import { createClientAnalytics } from 'trakoo/client';
import type { AppEvents } from './events';
export const analytics = createClientAnalytics<AppEvents>({
providers: [/* ... */]
});
The same generic applies on the server:
import { createServerAnalytics } from 'trakoo/server';
import type { AppEvents } from './events';
export const serverAnalytics = createServerAnalytics<AppEvents>({
providers: [/* ... */]
});
From there, your editor completes event names, then the property object for whichever event you picked, then the allowed values for each union property. Each event carries its own property type, so track('user_signed_up', …) and track('button_clicked', …) expect different shapes.
analytics.track('user_signed_up', {
email: 'ada@example.com'
// compile error: property 'plan' is missing
});
Type your user traits
createClientAnalytics takes a second generic for user traits. It types identify() the same way the first generic types track().
interface UserTraits {
email: string;
name: string;
plan: 'free' | 'pro' | 'enterprise';
company?: string;
role?: 'admin' | 'member' | 'viewer';
}
const analytics = createClientAnalytics<AppEvents, UserTraits>({
providers: [/* ... */]
});
analytics.identify('user_123', {
email: 'ada@example.com',
name: 'Ada Lovelace',
plan: 'pro',
role: 'admin'
// wrong: true // compile error — 'wrong' is not a UserTrait
});
Event categories
Every event has a category. trakoo ships a set of common ones through the EventCategory type, and you can add your own with as const.
import type { EventCategory } from 'trakoo';
const category: EventCategory = 'user';
// built-in: 'engagement' | 'user' | 'navigation' | 'error' | 'performance' | 'conversion'
export const appEvents = {
aiGenerated: {
name: 'ai_generated',
category: 'ai' as const, // custom category
properties: {} as { model: string }
}
} as const;
Complex property types
Properties are ordinary TypeScript types, so nested objects, arrays, and unions all work and stay checked at the call site.
export const appEvents = {
purchaseCompleted: {
name: 'purchase_completed',
category: 'conversion',
properties: {} as {
orderId: string;
currency: 'USD' | 'EUR' | 'GBP';
items: Array<{ productId: string; quantity: number }>;
shippingAddress: {
city: string;
country: string;
};
}
},
paymentProcessed: {
name: 'payment_processed',
category: 'conversion',
properties: {} as {
method: 'card' | 'paypal' | 'crypto';
amount: number;
cardDetails?: { last4: string; brand: string };
}
}
} as const satisfies EventCollection<Record<string, CreateEventDefinition<string>>>;
Extract types for reuse
Because the definition is a typed value, you can pull a single event’s property type out with typeof and reuse it in a helper, a form handler, or a shared function signature.
type UserSignedUpProps = typeof appEvents.userSignedUp.properties;
// { email: string; plan: 'free' | 'pro' | 'enterprise'; referralSource?: string }
function trackSignup(props: UserSignedUpProps) {
analytics.track('user_signed_up', props);
}
Best practices
Always end your collection with as const satisfies EventCollection<...>. as const keeps the literal types; satisfies validates the shape. Drop either and the types widen back to string, taking your autocomplete with them.
// Good — literal types preserved, shape validated
export const appEvents = { /* ... */ } as const satisfies EventCollection<Record<string, CreateEventDefinition<string>>>;
// Avoid — types widen to string, autocomplete is lost
export const appEvents = { /* ... */ };
- Prefer specific unions over
string.plan: 'free' | 'pro' | 'enterprise'catches typos thatplan: stringwaves through. - Mark optional properties with
?rather than| undefined, so callers can omit them entirely. - Export
type AppEvents = typeof appEventsnext to your definitions, and import it wherever you create an analytics instance.
