Skip to content
trakoo
Esc
navigateopen⌘Jpreview
On this page

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 that plan: string waves through.
  • Mark optional properties with ? rather than | undefined, so callers can omit them entirely.
  • Export type AppEvents = typeof appEvents next to your definitions, and import it wherever you create an analytics instance.

Next steps

Was this page helpful?