Type Safety
Infer event inputs, validated outputs, and user traits from one runtime registry.
trakoo binds each analytics instance to an event registry. Passing that registry value to a factory gives track() exact wire names and the correct properties for each name, without event generics or manual collection types.
Why it matters
Analytics mistakes are quiet: a typo can create a second event or an unusable property and remain unnoticed until a report is wrong. A registry-bound instance catches them while you write code.
await analytics.track('user_signed_up', {
email: 'ada@example.com',
plan: 'pro'
});
// TypeScript rejects:
// - 'user_signedup' because it is not a registry wire name
// - 'emai' because it is not a property
// - 'premium' because it is outside the plan union
Define the registry
import { defineEvents, noProperties, typed } from 'trakoo';
export const appEvents = defineEvents({
userSignedUp: {
name: 'user_signed_up',
category: 'user',
properties: typed<{
email: string;
plan: 'free' | 'pro' | 'enterprise';
referralSource?: string;
}>()
},
sessionStarted: {
name: 'session_started',
category: 'user',
properties: noProperties()
}
});
defineEvents() preserves the name and category literals while checking every definition. typed<T>() declares a validator-free object shape. noProperties() declares an event whose call has no properties argument.
No const assertion, collection interface, or exported event generic is required.
Infer at the factory
import { createClientAnalytics } from 'trakoo/client';
import { appEvents } from './events';
export const analytics = createClientAnalytics({
events: appEvents,
providers: [/* ... */]
});
The server factory infers from the same value:
import { createServerAnalytics } from 'trakoo/server';
import { appEvents } from './events';
export const serverAnalytics = createServerAnalytics({
events: appEvents,
providers: [/* ... */]
});
Both factories return fresh, independent instances. The application owns the returned instance and can export it from its own module for call sites to import.
Per-event call signatures
The registry produces a different tuple for each wire name:
await analytics.track('user_signed_up', {
email: 'ada@example.com',
plan: 'pro'
});
await analytics.track('session_started');
The first call requires properties. The second rejects a properties argument. On the server, property-bearing events accept an optional third options argument, while propertyless events accept options directly as the second argument.
Type user traits
Traits use the same validator-free marker:
import { typed } from 'trakoo';
import { createClientAnalytics } from 'trakoo/client';
import { appEvents } from './events';
interface UserTraits {
email: string;
name: string;
plan: 'free' | 'pro' | 'enterprise';
company?: string;
role?: 'admin' | 'member' | 'viewer';
}
const analytics = createClientAnalytics({
events: appEvents,
userTraits: typed<UserTraits>(),
providers: [/* ... */]
});
analytics.identify('user_123', {
email: 'ada@example.com',
name: 'Ada Lovelace',
plan: 'pro',
role: 'admin'
});
userTraits changes trait typing only. The event registry is still inferred from events.
Complex property types
Nested objects, array-valued fields, optional fields, and unions work naturally inside the top-level object shape:
const commerceEvents = defineEvents({
purchaseCompleted: {
name: 'purchase_completed',
category: 'conversion',
properties: typed<{
orderId: string;
currency: 'USD' | 'EUR' | 'GBP';
items: Array<{ productId: string; quantity: number }>;
shippingAddress: {
city: string;
country: string;
};
}>()
},
paymentProcessed: {
name: 'payment_processed',
category: 'conversion',
properties: typed<{
method: 'card' | 'paypal' | 'crypto';
amount: number;
cardDetails?: { last4: string; brand: string };
}>()
}
});
The top-level properties type itself must be a non-array, non-callable object shape.
Input and provider-output types
With typed<T>(), input and output are both T. A direct Standard Schema validator can have different input and output types because it parses or transforms data:
import { defineEvents } from 'trakoo';
import { z } from 'zod';
const commerceEvents = defineEvents({
orderCompleted: {
name: 'order_completed',
category: 'conversion',
properties: z.object({
orderId: z.string(),
amount: z.coerce.number().positive()
})
}
});
Standard Schema is an interface supported by validator libraries, not a required trakoo runtime. The schema input drives track() arguments; its validated output is what providers receive.
Use the shared type maps when another function needs one side explicitly:
import type { EventInputMap, EventOutputMap } from 'trakoo';
type OrderInput = EventInputMap<typeof commerceEvents>['order_completed'];
type OrderOutput = EventOutputMap<typeof commerceEvents>['order_completed'];
function trackOrder(input: OrderInput) {
return commerceAnalytics.track('order_completed', input);
}
OrderInput follows the schema input. OrderOutput is the parsed object delivered to providers.
Event categories
The EventCategory shared type includes common categories while allowing domain-specific strings:
import { defineEvents, typed, type EventCategory } from 'trakoo';
const category: EventCategory = 'user';
const aiEvents = defineEvents({
responseGenerated: {
name: 'ai_response_generated',
category: 'ai',
properties: typed<{ model: string }>()
}
});
Compile time and runtime
Compile-time checks protect typed call sites. At runtime:
defineEvents()provides the registry lookup.typed<T>()checks only that a property-bearing event receives an object.noProperties()rejects a supplied properties argument.- A direct Standard Schema validator validates and can transform individual fields.
Validation failures drop by default or throw when configured. They do not retain the input payload. See Events for the full policy.
Best practices
- Prefer
typed<T>()until runtime validation or transformation has a concrete purpose. - Use specific unions instead of broad
stringvalues when the domain is closed. - Mark optional properties with
?. - Use
noProperties()instead of an empty object type. - Pass the same registry value through
eventseverywhere; do not add factory event generics. - Use
userTraits: typed<UserTraits>()for custom traits.
