---
title: "Typed Fields"
description: "Add compile-time type safety to your wide events with TypeScript module augmentation. Prevent typos and ensure consistent field names across your codebase."
canonical_url: "https://www.evlog.dev/learn/typed-fields"
---
# Typed Fields

> Add compile-time type safety to your wide events with TypeScript module augmentation. Prevent typos and ensure consistent field names across your codebase.

By default, `useLogger` accepts any fields, which is great for getting started. But as your codebase grows, inconsistencies creep in: one route logs `user`, another logs `account`, a third logs `userId`. Typed fields solve this with opt-in compile-time safety.

<typed-fields-intellisense>



</typed-fields-intellisense>

## Basic Usage

Define an interface for your fields and pass it as a generic to `useLogger`:

```typescript [server/api/checkout.post.ts]
import { useLogger } from 'evlog'

interface CheckoutFields {
  user: { id: string; plan: string }
  cart: { items: number; total: number }
  action: string
}

export default defineEventHandler(async (event) => {
  const log = useLogger<CheckoutFields>(event)

  log.set({ user: { id: '123', plan: 'pro' } })  // OK
  log.set({ cart: { items: 3, total: 9999 } })    // OK
  log.set({ action: 'checkout' })                  // OK

  log.set({ account: '...' })                      // TS error
  log.set({ usr: { id: '123' } })                  // TS error

  return { success: true }
})
```

TypeScript then catches a typo or an unknown field at compile time, which is the only place a field-name mistake is cheap: once the event is in your drain the wrong key is already indexed, already queried, and already in someone's dashboard.

## Internal Fields

Some fields evlog sets itself. `status` and `service` are always accepted whatever your type says, through `InternalFields`:

```typescript [services/checkout.ts]
log.set({ status: 200 })    // OK - internal field
log.set({ service: 'api' }) // OK - internal field
```

You don't need to include `status` or `service` in your interface.

## Untyped Usage

Without a generic, `useLogger` accepts any fields as usual:

<framework-tabs>

```typescript [Nuxt]server/api/example.ts
import { useLogger } from 'evlog'

const log = useLogger(event)

log.set({ anything: true, nested: { deep: 'value' } }) // OK
```

```typescript [Next.js]app/api/example/route.ts
import { useLogger } from '@/lib/evlog'

const log = useLogger()

log.set({ anything: true, nested: { deep: 'value' } }) // OK
```

```typescript [Hono]src/index.ts
const log = c.get('log')

log.set({ anything: true, nested: { deep: 'value' } }) // OK
```

```typescript [Express]src/index.ts
const log = req.log

log.set({ anything: true, nested: { deep: 'value' } }) // OK
```

</framework-tabs>

Typed fields are fully opt-in.

## Nuxt Auto-Import

<callout color="warning" icon="i-lucide-triangle-alert">

Typed fields with `useLogger<T>` need an explicit import. The auto-import cannot carry excess property checking through a generic, a TypeScript limitation rather than a module one.

</callout>

```typescript [server/api/checkout.post.ts]
// Works - explicit import preserves type checking
import { useLogger } from 'evlog'
const log = useLogger<MyFields>(event)
log.set({ typo: 'oops' }) // TS error

// Does NOT work - auto-import loses excess property checking
const log = useLogger<MyFields>(event)
log.set({ typo: 'oops' }) // No error (silently accepted)
```

Untyped usage keeps the auto-import. Add the explicit one only where you pass a generic.

## Outside Nuxt

The same generic works with `createRequestLogger` and `createWorkersLogger`:

<framework-tabs>

```typescript [Standalone]
import { createRequestLogger } from 'evlog'

interface MyFields {
  action: string
  userId: string
}

const log = createRequestLogger<MyFields>({
  method: 'POST',
  path: '/checkout',
})

log.set({ action: 'checkout', userId: '123' }) // OK
log.set({ unknown: true })                      // TS error
```

```typescript [Cloudflare Workers]
import { createWorkersLogger } from 'evlog/workers'

interface MyFields {
  action: string
}

const log = createWorkersLogger<MyFields>(request)
log.set({ action: 'process' }) // OK
```

</framework-tabs>

## Design Tips

### One Interface Per Domain

Define field interfaces per domain area, not per route:

```typescript [server/types/log-fields.ts]
export interface AuthFields {
  user: { id: string; email: string; role: string }
  action: string
  mfaUsed: boolean
}

export interface PaymentFields {
  user: { id: string; plan: string }
  order: { id: string; total: number; currency: string }
  payment: { method: string; last4: string }
}
```

```typescript [server/api/auth/login.post.ts]
import { useLogger } from 'evlog'
import type { AuthFields } from '~/server/types/log-fields'

export default defineEventHandler(async (event) => {
  const log = useLogger<AuthFields>(event)
  // ...
})
```

### Keep Interfaces Focused

Include only the fields your routes actually set. The interface doesn't need to mirror your entire data model:

```typescript [server/types/evlog.ts]
// Too broad - most routes won't set all these
interface EverythingFields {
  user: FullUserProfile
  order: CompleteOrder
  payment: PaymentDetails
  shipping: ShippingInfo
}

// Focused - only what this route sets
interface CheckoutFields {
  user: { id: string; plan: string }
  cart: { items: number; total: number }
}
```

## Next Steps

- [Wide Events](https://www.evlog.dev/learn/wide-events): Design effective wide events with context layering
- [Best Practices](https://www.evlog.dev/reference/best-practices): Security guidelines for preventing sensitive data leakage
- [Configuration](https://www.evlog.dev/reference/configuration): All `initLogger` and middleware options

---

- [Wide Events](https://www.evlog.dev/learn/wide-events)
- [Best Practices](https://www.evlog.dev/reference/best-practices)


## Sitemap

See the full [sitemap](https://www.evlog.dev/sitemap.md) for all pages.
