Your decorator library silently breaks on Vite. Here's why (and a fix) — ts-jackson 2.0

Your decorator library silently breaks on Vite. Here's why (and a fix) — ts-jackson 2.0

calendar_today agoschedule2 min read
— Originally published at dev.to

You have a class that maps an API response. It works perfectly in your old webpack + ts-loader project:

@Serializable()
class Event {
  @JsonProperty()
  startsAt: Date

  @JsonProperty()
  attendees: number
}

You move to Vite. It compiles. It runs. No warnings. And then, somewhere far from this file, production throws event.startsAt.getTime is not a function — because startsAt is quietly a plain string now, and attendees might be one too.

Nothing crashed at the mapping site. That's what makes this failure mode nasty. Here's what's actually going on.

Why: the two-flag magic trick

Every classic decorator-metadata library — class-transformer, TypeORM, typescript-json-serializer, ts-jackson 1.x — relies on two tsconfig flags:

{
  "experimentalDecorators": true,
  "emitDecoratorMetadata": true
}

The first enables the legacy decorator syntax. The second is the magic one: it makes the TypeScript compiler look at your type annotation (startsAt: Date) and secretly emit a runtime value next to the decorator:

// what tsc actually generates (simplified)
Reflect.metadata('design:type', Date)

That's how @JsonProperty() with zero configuration knows it should build a Date. The type annotation — normally erased at compile time — gets smuggled into the runtime.

Here's the catch: emitting design:type requires actually resolving types. Is startsAt's annotation a class? An interface? An import alias? Answering that means type-checking the program.

Now recall why esbuild is fast: it transpiles file-by-file and never type-checks. It just strips annotations. So esbuild doesn't implement emitDecoratorMetadata — not as an oversight, but because implementing it would require becoming the thing it was built to replace. And Vite transforms your TypeScript with esbuild.

The result on a stock Vite project:

Reflect.getMetadata('design:type', target, 'startsAt') // undefined

The decorator still runs. Metadata is just… missing. Your mapping library falls back to "no type known", passes the raw JSON value through, and you get a string where a Date should be. No error at the site of the lie — only downstream, at runtime, in whatever code first trusts the type.

(SWC, for the record, can emit decorator metadata, but it's opt-in config — stock setups frequently miss it too.)

The fix: decorators that don't need a compiler accomplice

Since 5.0 — with decorator metadata following in 5.2, TypeScript's default decorator mode — no flags at all — implements the TC39 standard decorators proposal, together with the decorator-metadata proposal (Symbol.metadata). Standard decorators are pure runtime semantics: the decorator receives a context object, stores whatever metadata it wants, and no compiler magic is involved anywhere.

Which means they work under esbuild. And Vite. And every bundler that can handle modern JavaScript — because there's nothing to implement beyond the syntax itself.

There's one honest trade-off: the standard has no equivalent of design:type, by design — JavaScript has no types at runtime, and TC39 doesn't standardize compiler tricks. So type inference from annotations is gone; where conversion matters, you declare the type explicitly.

ts-jackson 2.0: both worlds, auto-detected

I just shipped ts-jackson 2.0, and the headline feature is exactly this: it supports both decorator modes, and detects at runtime which one invoked it. Same imports, same API.

Legacy mode (tsc, flags on) — full type inference, exactly like v1:

@Serializable()
class Event {
  @JsonProperty()
  startsAt: Date            // inferred from the annotation
}

Standard mode (Vite, esbuild, no flags) — explicit types:

@Serializable()
class Event {
  @JsonProperty({ type: Date })
  startsAt: Date

  @JsonProperty({ elementType: Image })  // elementType implies an array
  images: Image[]
}
🔥 Join developers growing publicly
Share your knowledge, build in public, and grow your developer presence with a global community.

More Posts

TypeScript Complexity Has Finally Reached the Point of Total Absurdity

Karol Modelski - Apr 23

I’m a Senior Dev and I’ve Forgotten How to Think Without a Prompt

Karol Modelski - Mar 19

5 Web Dev Pitfalls That Are Silently Killing Your Projects (With Real Fixes)

Dharanidharan - Mar 3

Your Tech Stack Isn’t Your Ceiling. Your Story Is

Karol Modelski - Apr 9

SolidJS 2.0 Async Data: A Deep Dive for React Devs

morellodev - Jul 16
chevron_left
121 Points3 Badges
Spain, Barcelona
1Posts
0Comments

Commenters (This Week)

2 comments
1 comment
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!