When decorators don't decorate - My TypeScript Misadventure

When decorators don't decorate - My TypeScript Misadventure
Foto von CHUTTERSNAP auf Unsplash

Back in October 2021, I started on writing "jb_serialize," a serialization library in TypeScript using decorators. The idea was to define "Models" as TypeScript classes and use decorators to provide additional information on how to parse/validate and serialize and deserialize them. The initial attempts looked promising:

@Serializable()
export class TicketDraft extends BasicSerializable {
    @SerializableStringProperty({
        database: {
            typeHint: 'VARCHAR(255)'
        }
    })
    declare name: string
}
@Serializable({
    database: {
        schema: 'tickets',
        autoHistory: PROJECT_AUTO_HISTORY_NAMES.tickets,
    },
})
export class Ticket extends MixedSerializable([TicketDraft, DBAuditableItemSerialId]) {

}

So I continued building various other parts of my software stack around this library:

  • jb_api: Utilizes serialized objects to define API interfaces between the frontend and backend, enabling type-safe API calls without requiring the frontend to parse, validate, or serialize the data.
  • jb_memdb: A database used in the web worker on the frontend to quickly perform "live-queries" on serialized objects.
  • Postgres schema with auto-history: Additional annotations on the jb_serialize models allowed me to generate a full PostgreSQL schema from the model definitions, along with automatic history tracking via triggers and live updates (utilizing pg_notify).
  • form generatin and validation using felte

While these integrations worked nicely, I constantly battled with the decorators themselves, especially when changing build and testing behavior. I can't fully remember how many hours I've spent here, but I can say if I had known beforehand, I would have taken a different road.

When TypeScript 5.0 introduced new decorators, I attempted to upgrade my jb_serialize library, hoping for fewer issues. However, it failed again—my testing and build frameworks did not support the new decorators at that time.

Why is it doomed to fail?

When using the TypeScript Compiler (tsc) directly, decorators work as expected because the compiler has all the necessary information. However, no one uses tsc directly in larger projects because it’s slow. Tools like esbuild, Vite, and Babel are much faster alternatives.

These tools are faster because they are "transpilers" rather than full compilers. They convert TypeScript to JavaScript but essentially discard the type information. This lack of type information is why they are so fast but also why they struggle with decorators.

For example, consider the following TypeScript function:

function func(param: string): void {
    console.log(param);
}

At compile time, TypeScript ensures param is a string. However, the resulting JavaScript looks like this:

function func(param) {
    console.log(param);
}

The type hints are gone because JavaScript doesn’t support them. Transpilers strip out the type hints and generate JavaScript without performing the actual type checking, allowing them to be faster. However, this means they can't handle reflection and decorators unless you write plugins to hack it in somehow.

You might ask if it’s dangerous to throw away type-checking and if we should use transpilers at all. Generally, it's fine—these tools often perform optimized type-checking in separate processes, so you still get compile errors if something is wrong.

Alternatives

Instead on relying on decorators I should have built it in a way zod or superstruct does. Let's check an example:

import { z } from 'zod';

const User = z.object({
    name: z.string(),
    age: z.number(),
});

type User = z.infer<typeof User>;

They types are not annotated but real Objects from the libary. There's typescript-utilities to create a "raw typescript type" from the definition as well. You can extend and merge objects, define own types, etc. No limits. It just doesn't look "as fancy" in my eyes - but well - you get used to it really quickly and it's much better than constantly have to fight with the build tools.

Conclusion

While I started using and loving typescript as of version 0.8 beta (in 2013 I believe?), my journey with TypeScript decorators has been fraught with challenges. The integration issues and lack of support in popular build tools made the process more trouble than it was worth. If you’re considering using decorators in TypeScript, be aware of these potential pitfalls and consider whether the benefits outweigh the headaches.
I am currently checking the effort to migrate my other software to using zod and to hack in additional meta information there for my database hints and  other things (maybe using "describe")