r/typescript • • 43m ago

Every refactor in our NestJS repo turns into fixing u/Traced decorators

• Upvotes

NestJS monorepo, eleven services. Two years ago we wrote a `@Traced()` decorator that wraps every service method in an OpenTelemetry span, plus a typed helper so attribute names come from one union type and nobody writes `userId` in one place and `user_id` in another.

Now every refactor drags it along. Rename a method and the span name changes, so the Grafana panel built on the old name goes quietly empty and nobody notices for weeks. Move logic into a plain function and it stops being traced, because the decorator only works on class methods. The SDK upgrade in July broke the helper's types and I lost most of a sprint to generics so a tracing wrapper would compile. The PR template has a "spans updated?" checkbox and everyone ticks it.

One service has hud on it as a trial instead of more decorators. There's no manual instrumentation, it picks up the functions on its own, and after the last deploy it had the renamed ones without anyone touching a config. It doesn't build the business dashboards product wants though, signups per plan and that kind of thing, so Grafana and a handful of spans stay.

The decorator stays where it is on the quiet services. New code just doesn't get it anymore.


r/typescript • • 23h ago

End-to-end types over WebSockets: the server documents itself as AsyncAPI, the client is generated from it

2 Upvotes

Hi all, I want to share an open-source TypeScript library I built (chanx-js, MIT) to type both ends of a WebSocket from one source, without sharing a types package between the frontend and the backend.

With REST we have OpenAPI and generated clients. With WebSockets I kept ending up with JSON.parse(event.data) giving me any, a hand-written union of message types on the client, and nothing stopping it from drifting away from what the server really sends.

So in chanx-js:

  1. On the server you declare messages with any Standard Schema library (Zod, Valibot, ArkType), and the payload types are inferred from the schema.
  2. The server serves an AsyncAPI 3 document built from those handlers.
  3. npx @chanx-js/codegen http://localhost:8000/asyncapi.json generates the client types from it.

On the server, the path params, the payload and what the handler may send are all typed:

const Chat = defineMessage('chat', z.object({ text: z.string() }));
const Notice = defineMessage('notice', z.object({ text: z.string() }));

consumer('/ws/chat/{room}', { name: 'chat' }).on(Chat, { output: Notice }, (m, ctx) => {
  m.payload.text; // string
  ctx.params.room; // string, parsed from the path by a template literal type
  return Notice.create({ text: m.payload.text }); // anything but Notice is an error
});

On the client, request() knows the reply type, and a wrong payload doesn't compile:

const connection = client.connect(chat, { params: { room: 'lobby' } });

const reply = await connection.request({ action: 'chat', payload: { text: 'hi' } });
reply.payload.text; // string, from the handler's `output`
connection.send({ action: 'chat', payload: { txt: 'hi' } }); // compile error

The contract goes through AsyncAPI instead of shared TS code, so the server doesn't have to live in the same repo, or even be written in TypeScript.

Repo: https://github.com/chanx-dev/chanx-js

It's still new, so if you run into problems or have ideas for the types, please open an issue or a discussion. Stars and PRs are welcome too.