Skip to content

TypeScript codegen

@syntarie/tracking/generated/events is a generated file: the codegen tool reads your tracking plan and emits a typed track() whose name argument is narrowed to your registered event names and whose props argument is narrowed to the per-event interface.

// Without codegen, string literals and untyped props:
import { track } from '@syntarie/tracking';
track('checkout_compleded', { plan: 'pro' });
// ^^^^^^^^^^^^^^^^^^ typo, accepted at compile time, dropped on ingest
// With codegen, names and props checked at compile time:
import { track } from '@syntarie/tracking/generated/events';
track('checkout_compleded', { plan: 'pro' });
// ~~~~~~~~~~~~~~~~~~~~ Type error: argument of type "checkout_compleded"
// is not assignable to parameter of type
// TrackedEventName.

Behind the scenes the runtime function is the same untyped track. The generated subpath only adds compile-time overloads, so there is no runtime overhead and no extra bytes shipped if you import only from the generated path.

The codegen CLI ships with @syntarie/tracking. Point it at your tracking plan and an output path:

Terminal window
pnpm exec leadmaps-codegen --plan ./tracking-plan.yaml --out ./src/generated/events.ts

This:

  1. Reads your tracking-plan YAML.
  2. Validates the plan against the tracking-plan schema.
  3. Emits a typed-track() module with one overload per event.

Run codegen as a prebuild step in CI so the generated output never drifts from the source plan.

For a plan event like:

checkout_completed:
description: User finished checkout and a payment intent succeeded.
owner: ecommerce
classification: revenue
props:
type: object
required: [order_id, total_cents, currency]
additionalProperties: false
properties:
order_id: { type: string, minLength: 1 }
total_cents: { type: integer, minimum: 0 }
currency: { type: string, pattern: "^[A-Z]{3}$" }
coupon_code: { type: string }

The generator emits roughly:

export interface CheckoutCompletedProps {
readonly order_id: string;
readonly total_cents: number;
readonly currency: string;
readonly coupon_code?: string;
}
export interface TrackedEvents {
// ...other events...
readonly checkout_completed: CheckoutCompletedProps;
}
export type TrackedEventName = keyof TrackedEvents;
export function track<N extends TrackedEventName>(
name: N,
props: TrackedEvents[N],
): void;

You can extend TrackedEvents via TypeScript declaration merging if you need to track an event before it lands in the central plan:

declare module '@syntarie/tracking/generated/events' {
interface TrackedEvents {
readonly experimental_event: { readonly variant: string };
}
}

The runtime path is unchanged. The merge only adds a compile-time narrowing for that name.

Adding a new event to the plan and re-running codegen is a minor bump of @syntarie/tracking (a new key on TrackedEvents). Removing an event inside a major is forbidden by the semver policy. See Versioning §1.2.