r/typescript • • 4h ago

I've spent 3 years on a TypeScript library for generating Excel reports from a typed schema, v5 just shipped

9 Upvotes

Hey r/typescript,

I've been building typed-xlsx for about 3 years now, it's a library for generating Excel reports from TypeScript : https://typed-xlsx.vercel.app

It only writes files, so for reading or editing existing spreadsheets SheetJS or ExcelJS are what you want. But if you have typed data and need to turn it into a structured report, from a 3 column export to a multi-sheet pack with formulas, totals, charts and pivots, I honestly think it's the best option out there.

You describe the columns once, typed against your row type:

type Invoice = {
  id: string;
  customer: { name: string };
  qty: number;
  unitPrice: number;
};

const invoices = createExcelSchema<Invoice>()
  .column("id", { header: "Invoice #", accessor: "id" })
  .column("customer", { header: "Customer", accessor: "customer.name" })
  .column("qty", { header: "Qty", accessor: "qty" })
  .column("price", { header: "Unit price", accessor: "unitPrice" })
  .column("total", {
    header: "Total",
    // written as =(C2*D2) in the file
    formula: ({ refs }) => refs.column("qty").mul(refs.column("price")),
    summary: (s) => [s.formula("sum")],
  })
  .build();

The same schema works for any size of export:

// in memory
const workbook = createWorkbook();
workbook.sheet("Invoices").table("invoices", { rows, schema: invoices });
await workbook.writeToFile("./invoices.xlsx");

// streamed, e.g. 500k rows from a DB cursor
const stream = createWorkbookStream();
const table = await stream
  .sheet("Invoices")
  .table("invoices", { schema: invoices });

for await (const batch of cursor) {
  await table.commit({ rows: batch });
}
await stream.pipeToNode(res);

It's fast: 200k styled rows stream in about 3.5s with memory flat around 275 MiB, roughly 3x faster than v4. Zero dependencies, runs on Node, Bun, Deno and Cloudflare Workers.

Files look clean with no styling at all, and branding is a theme with a few colors. Per-row styles and native Excel conditional formatting are there when you need them.

The types go past accessors: a typo like customer.nam gets a "did you mean", a checkbox column only accepts booleans, a chart's y has to be numeric, and formulas reference columns by id so a wrong id doesn't compile. Formulas are optional, you can also just compute values in TS.

Also in there: native Excel tables, dynamic columns, sub-rows, dropdowns and validation, sheet protection, hyperlinks, images, sparklines and more. The playground has full workbooks for most of it.

It ships an agent skill too (npx skills add ChronicStone/typed-xlsx), and since everything is typed, tsc tells the agent when it got something wrong.

Repo: https://github.com/ChronicStone/typed-xlsx Docs: https://typed-xlsx.vercel.app Playground: https://typed-xlsx.vercel.app/playground

Feedback welcome, especially if you've built Excel exports in TS before.


r/typescript • • 1h ago

Nx monorepo with Node.js + Angular projects

2 Upvotes

We are currently using Nx (v23.x) to manage multiple interdependent projects. The monorepo contains both Node.js packages and Angular applications/libraries.

We still rely on TypeScript v6 and path aliases instead of project references, as Angular does not support them yet.

Is there anyone who's managed to have both TypeScript v7 and v6 on the same Nx monorepo, and correctly build Node.js packages with v7, and Angular applications/libraries with v6? Is this even possible? Note that Angular projects may reference shared libraries (that should be buildable in isolation with TS7 too).

I'm looking for others' experiences as I don't want to find myself on TS6 forever. The status quo is not great honestly.


r/typescript • • 4h ago

trpc-demo-link: Build demos using tRPC client links that mock your API without changing your app/components

Thumbnail
github.com
1 Upvotes

Hi all ! Wanted to share this example repository I made that shows how I build demos using tRPC by minimally mocking routes that are called while viewing the demo (organization).

My TypeScript types are probably cursed but they enforce that newly introduced routes that could break the demo must be mocked. If you you tRPC or have a similarly-shaped SPA this could be useful for you!


r/typescript • • 3h ago

It Depends - TypeScript Debated

0 Upvotes

I wrote this book, and it took me nearly a year to complete. It's about TypeScript practices. It's opinionated, and it's meant to be a little "lighter" to read.

The idea is pretty simple: instead of writing another conventional technical book that tells you what to do, I wanted to build something around debates between developers. Different personalities, different philosophies, different approaches to solving problems. They argue, challenge each other's assumptions, bring working code into the discussion, and sometimes even change their minds.

And here's the thing: not every debate has a clear winner. Sometimes there's a verdict, sometimes the discussion remains open. Because that's how software engineering actually works.

Let me address the question you're probably all thinking right now: Yes, I used AI. Extensively.

However, all the scripts are mine. The voices are mine, the subjects are mine, the debates are mine.

So, what do I mean when I say AI was used extensively?

It helped shape the book and organize its content. It helped with spelling and grammar, refining tones, reviewing the material, making sure all the examples actually work, shaping the chapters themselves, and even helping with the printing process (there's a print version!) and the cover.

I didn't let AI write the text by itself. That was my one rule when working with AI on this book.

The book is free and open source. You don't have to pay a dime to read it. If you prefer a physical copy, there's also a print version available on Amazon, which, for obvious reasons, does cost some money.

Anyway, I'd genuinely love to hear what you think. Whether it's feedback about the content, the format, the opinions, or even the way the book was written.

you can read it here https://eliraz-refael.github.io/it-depends-ts-book/

In there you can find the link to the print version as well.


r/typescript • • 2d ago

getElementById with type guard and variable name guard

12 Upvotes

I've been trying to write a TypeScript wrapper around document.getElementById. I've got something that I'm almost happy with, so I'll start with that, then I'll explain my motivations and you can decide whether this makes sense or not, or which of my assumptions is most wrong.

There's a type guard, which means it will throw an exception if document.getElementById returns an element of the wrong type.

There's also what I'm calling a variable name guard. The wrapper returns an object with a single property with the same name as the html id. This stops the variable name from getting out of sync with the id accidentally - a mistake I seem to be quite prone to, I'm afraid.

Here's the code:

export const getElementById = <K extends string, V extends HTMLElement>(
    elementType: { new (): V },
    id: K,
): Record<K, V> => {
    const elm = document.getElementById(id);

    if (elm instanceof elementType) {
        return { [id]: elm } as Record<K, V>;
    } else if (elm === null) {
        throw new Error(`Element ${id} not found`);
    } else {
        throw new Error(`Element ${id} is of unexpected type, ${elm.nodeName}`);
    }
};

And it's called like this:

import { getElementById } from './getElementById';

const { mmEditor } = getElementById(HTMLTextAreaElement, 'mmEditor');
const { parseButton } = getElementById(HTMLElement, 'parseButton');
const { parseOutput } = getElementById(HTMLElement, 'parseOutput');
const { mmpEditor } = getElementById(HTMLTextAreaElement, 'mmpEditor');
const { unifyButton } = getElementById(HTMLElement, 'unifyButton');
const { unifierOutput } = getElementById(HTMLElement, 'unifierOutput');

When presented with vanilla html DOM code like this, the first question I would always ask is: why haven't I used a web-framework?

In this case, there's a package that I've written, and I wish to demonstrate its use in a web environment. So I'm trying to be inclusive, and write something you can hopefully follow whether your preferred framework is React, Angular, Vue, etc. It being demo code also explains why I wanted nice clearly worded exceptions and then made no effort to catch them.

I have unconsciously excluded JavaScript developers, however. Previously I have written such examples in JavaScript in the hope to include everyone. But this time I automatically chose TypeScript and didn't realize what I'd done until I'd almost finished it. It's 2026, is it OK to exclude JavaScript devs?

The only thing that makes me unhappy about this code is the 'as' statement. You should be able to satisfy yourself that it's correct and the type can't be anything else, but - and this is kind of the point of using a typed language - I'd rather you didn't have to. Unfortunately it's an inference that TypeScript itself doesn't appear to be able to make, and I can't find a way around that.

The elementType which is passed in might be worth elaborating on. I started with one function per element type, and when I combined them, I ended up with this, which would be nice if it would compile

const getElementId = <T extends HTMLElement>(id: keyof Record<string, T>) => {
    const elm = document.getElementById(String(id));
    if (elm instanceof T) {
        return { [id]: elm } as Record<string, T>;
    } else {
        throw new Error(`Element ${id} not found or wrong type`);
    }
}

src/index.ts:23:24 - error TS2693: 'T' only refers to a type, but is being used as a value here.

But it's hobbled by the need to transpile into JavaScript and can't take the type with it. So we pass the elementType in instead, which is defined as an object which has a constructor returning the expected element type. That we can do an instanceof on.

Thanks for reading. It's been nice to have a small piece of code with an easily understood purpose to talk about. That doesn't happen very often. When I start writing a program I'm happy to put any old runtime type check in, just to pander to the compiler's whims, but here I've finished up with something that does make it easy to hack around with a piece of code because it tells me clearly when I inevitably screw up. Anything with a generic in it is always a fun puzzle, I find, and I mean that both in a good and a bad way. I hope you found this interesting, what do you think?


r/typescript • • 2d ago

What happens when a side project you abandoned years ago suddenly becomes relevant again?

0 Upvotes

A few years ago, when Deno was first announced, I started building a NestJS-style framework that could run on both Node.js and Deno.

Like many side projects, it eventually ended up collecting dust.

But the JavaScript ecosystem kept evolving. New runtimes and environments emerged, like Bun and Edge runtimes, and I realized the original idea was more relevant than ever.

So I dusted off the code, rethought a few things, and brought the project back to life.

Today, I'm releasing the first public beta of CHO (which, somewhat amusingly, is already at version 3!) to gather feedback from the community.

CHO runs on every JavaScript/TypeScript runtime, uses native JavaScript decorators instead of TypeScript's legacy decorators, and has zero dependencies.

It's still early days, and I'd love to hear what you think, especially from developers interested in framework design and modern JavaScript.

🦋 https://github.com/cho-js/cho


r/typescript • • 3d ago

Surprising type of a filtered array

4 Upvotes

I have an array that contains a mix of number and string values, in other words its type is (string | number)[]:

a = [0, 1, 2, NaN, 'x']

I wanted to filter only the numeric values 0, 1 and 2 that are not NaN, so I used a perfectly ordinary filter() call:

numbers = a.filter(n => typeof n === 'number' && !isNaN(n))

However, to my surprise the array "numbers" still has type (string | number)[], which logically makes no sense. No string value can pass the filter.

However, if I split the "and" expression to two separate filter() calls the array does get type number[]:

numbers = a.filter(n => typeof n === 'number').filter(n => !isNaN(n))

What's the reason for this surprising behaviour? Is there a general TypeScript principle that applies here?

I'm experienced in JavaScript, however not super well-versed in TypeScript.


r/typescript • • 2d ago

A package for standardizing logging? Looking for thoughts

0 Upvotes

Hey!

How do you guys currently approach event logging?

After reading https://loggingsucks.com/ (like about a year ago), I thought of creating a small TypeScript package where you define a logger with a specific expected output log format, then the tool is used to add context/data during an event/request and finally the tool writes one structured log at the end checking for missing fields.
I've never actually did it, because I doubted that it'd be useful. I also heard pinojs has more or less something like this, but without any actual validation. However, recently I've quickly wrapped up a PoC tool.

The tool (LogPal): https://pages.doteric.net/logpal/ (Of course a lot of it is "vibe code", but it's just a PoC)

It’s a proof of concept for now. If the idea seems useful, I can definitely put the work into making it production ready. I wanted something that you don't have to do almost any configuration, you can just use it out of the box and if really needed then add some extra configuration.

Do you think it's worth investing into such tool? Or it'll become yet another logging tool like various different ones (pino, evlog, winston etc.)? Would you use something like this, or is it better handled within each project separately?


r/typescript • • 4d ago

Refactoring my vibecoded TS repo

0 Upvotes

TLDR: refactoring my codebase feature wise and wsnt to make sure this time it doesn't spiral out like the slop we did with claude in the last 6 months.

Stack: TS Express Prisma and Psql

So, I work as an SDE1 at a startup. Honestly, things got super messy around here because our managers just kept pushing for new features every single day. I finally decided I had to stretch out this project timeline. I am using this extra time to completely clean up my code and move it to a way more maintainable structure. I am ditching our messy, sort-of layered architecture and switching over to a vertical slicing setup. I am also adding clear readme files for every single module, putting up Pino for proper logging, and setting up the actual test suites we need.

This is what I want to ask the community, how can I make sure this migration hits all the right software development best practices? I really want to set things up so that Claude does not mess up my codebase ever again. Any Repo references or stack


r/typescript • • 6d ago

Introducing Skif - a repository/monorepo linter written in Rust

0 Upvotes

Hi Peeps,

I want to share with you a tool I am working on. Its still early, but I already use it and it works well.

So story goes like this - I was working on restructuring a very large monorepo with disparate package managers and versions into a single PNPM workspace. When I was finished I decided I am going to enforce certain conventions so it could not drift again without failing CI. I set this up, and found myself using several different tools -Sherif, Syncpack and Knip to ensure that the repository doesn't drift again. This setup, while working, is rather clanky and also suffered from some issues - mainly performance, but also a few bugs.

I therefore decided I will build my own tool. I forked Sherif as a baseline, and built on top of it while substantially rewriting and extending its capabilities. The result is skif: a Rust linter and enforcement tool for JS/TS dependencies and monorepos.

skif replaces sherif and syncpack, and most of what knip does (the aim is to offer a full replacement for Knip in the future) in a single, ultra fast, binary.

npx skif-cli              # lint the workspace or package you are in
npx skif-cli --fix        # apply safe fixes
npx skif-cli migrate      # turn sherif, syncpack and knip config into a skif.toml

Where it came from

skif started as a fork of sherif, and the workspace and manifest rules are ported from it. The version and catalog policies follow syncpack's model. The dead-code analysis follows knip's issue types.

It has since gone past the fork. It ports every sherif rule, adds new ones and fixes, and runs faster on every target measured. On the syncpack side it enforces version, range, banned-dependency and catalog policy.

What it deliberately does not do is sync versions. skif is a linter: it tells you a policy is broken and fails CI. Moving versions in step is left to specialized tools or the package manager's themselves. If you need this, use syncpack for example.

What it checks

42 rules:

  • Manifests and workspace: empty dependency maps, types in dependencies, root private and packageManager, unsorted dependencies, workspace globs that match nothing, members without a package.json.

  • Version policy: mismatched versions, semver range policy, banned dependencies, catalogs (missing entries, unused entries, references that do not resolve), workspace: protocol, peer dependency compatibility.

  • Dead code: unused files, exports, types, enum members and dependencies; unlisted dependencies and binaries; unresolved imports; circular imports. Leveraging the OXC ecosystem for this.

It also adds some new rules, such as incompatible-peer-dependencies and workspace-protocol.

Skif lifts the idea of framework plugins - which allow it to understand different types of entrypoints (e.g. Next.JS, Vite, Storybook etc.). But unlike Knip, which loads these files, Skif doesn't actually load code - it only statically analyses it. This removes a potential surface for code execution, and is altogether much faster, although it does bring in the potential to some imprecision.

Supported package managers

Skif supports all major package managers - npm, pnpm, Yarn classic, Yarn Berry as well as Bun. It does not support Deno, although if there is a need it might in the future. It also supports Rush and Lerna workspaces.

Benchmarks

Speed

Medians of 15 runs on a 4-vCPU Linux VM, pinned public repositories, dependencies installed:

Repositoryskifknip
create-t3-turbo32 ms2.0 s
vue-core62 ms1.7 s
tanstack-query375 ms4.5 s
npm-cli99 ms968 ms
babel884 ms7.2 s

Manifest checks alone (--no-js) against sherif and syncpack:

Targetskifsherifsyncpack
create-t3-turbo4.6 ms36 ms41 ms
babel9.4 ms37 ms280 ms
2,000 generated packages102 ms130 ms655 ms

sherif is already fast Rust, so the margin there is small: 1.3x at 2,000 packages. The real win is knip, and not paying for three processes.

Links

P.S.

Note: This is an early project, it is on v0.2.0. I am very eager to have users, since it will improve the tool and help me fix bugs. But do expect some issues!

Thanks for reading, this was a human written post (the benchmarking data was done by an LLM). If you find the tool useful, star it!


r/typescript • • 7d ago

libtmux-ts 0.1 - TypeScript Client for tmux

Thumbnail
github.com
0 Upvotes

I maintain libtmux, the Python library that's been around since 2016, and am porting it to more languages. This is the TypeScript / Node.js port. In many ways, this is more capable than the current Python implementation.

One of the main ways it differs is keeping immutable snapshots and typed querying of the tmux object graph (Server, Session, Window, Pane). It also supports tmux's control mode and batching of operations. Architecturally, libtmux for Python would have to break its API to adopt this snapshot model. For other libtmux ports, they don't have the encumbrance of worrying about API breakages to downstream users.

A word on AI: This project is experimental, and I used LLMs extensively to build it (with zero runtime dependencies and automated tests against live tmux(1) servers). I keep libtmux's AI policy at https://libtmux.org/en/meta/ai-policy/ based on Pallets', tmux's, and Java's.

There are four npm packages:

PackageWhat it doesSource
libtmuxSessions, windows, panes, queries and eventsGitHub
@libtmux/mcpMCP serverGitHub
@libtmux/workspaceBuild sessions from configuration objectsGitHub
@libtmux/workspace-cliLoad and capture YAML / JSON workspacesGitHub

The core also exports libtmux/selection, libtmux/engine and libtmux/testing. Those come with libtmux; there's nothing extra to install.

libtmux core: typed Node.js library for tmux

npm · GitHub · Homepage · Guides · API Explorer

What's interesting:

The installation commands use Node 22+ with npm and need tmux 3.3a+ installed locally. Save the examples as .mts files; they also run with Bun. The Node commands use tsx.

Install:

npm install libtmux

Save as demo.mts. It creates a separate tmux server, adds two windows, splits the editor window and names a pane using its title. It cleans up that server afterward.

The queries match a session name, window names and a pane title, with OR, a string prefix and relationships.

import { Server } from "libtmux";

const server = new Server({
  socketName: `libtmux-demo-${process.pid}`,
  configFile: "/dev/null",
});

const session = await server.newSession({ name: "demo" });

try {
  const [editor] = await server.batch([
    session.plan.newWindow({ name: "editor" }),
    session.plan.newWindow({ name: "logs" }),
  ]);
  const build = await editor.split({ shellCommand: "cat" });
  await build.setTitle("build");

  const snapshot = await server.snapshot();
  const matchedSession = snapshot.sessions.where({ name: "demo" }).one();
  const matches = snapshot.windows.where({
    session: { is: { name: "demo" } },
    OR: [
      { name: { startsWith: "edit" } },
      { name: "logs" },
    ],
    panes: { some: { active: true } },
  });

  const matchedPane = snapshot.panes.where({
    title: "build",
    session: { is: { name: "demo" } },
    window: { is: { name: "editor" } },
  }).one();

  console.log(`session: ${matchedSession.name}`);
  for (const window of matches) {
    console.log(`${window.name}: ${window.panes.length} panes`);
  }
  console.log(`pane: ${matchedPane.title}`);
} finally {
  await server.kill();
}

For Node, install tsx:

npm install --save-dev tsx

Run with Node:

node --import tsx demo.mts

Or Bun:

bun demo.mts

Or use the tsx command:

npx tsx demo.mts

Each prints:

session: demo
editor: 2 panes
logs: 1 panes
pane: build

P.S. Try changing the pane's window condition from "editor" to "editor2". .one() raises a NoMatchError carrying the query that failed. Even in the alpha, the library means to raise detailed, typed errors.

The error includes the query:

  code: 'NoMatchError',
  subcommand: undefined,
  query: {
    pane_title: 'build',
    session: { is: { name: 'demo' } },
    window: { is: { name: 'editor2' } }
  }

Planning operations, then querying the result

The earlier example uses session.plan to describe work, and server.batch to run it. Each creation normally reads a snapshot back; a batch shares that last read (example).

Save as batch.mts. This counts invocations through recordInvocations, creates the same three windows each way, then chains queries over one snapshot.

import { Server } from "libtmux";
import { recordInvocations } from "libtmux/testing";

const recorder = recordInvocations();
const count = () => recorder.recording().invocations.length;
const server = new Server({
  socketName: `libtmux-batch-${process.pid}`,
  configFile: "/dev/null",
  engine: recorder.engine,
});

try {
  const serial = await server.newSession({ name: "serial" });
  const session = await server.newSession({ name: "demo" });
  const names = ["editor", "logs", "shell"];

  let before = count();
  for (const name of names) {
    await serial.newWindow({ name });
  }
  console.log(`sequential: ${count() - before} invocations`);

  before = count();
  const [, logs] = await server.batch(
    names.map((name) => session.plan.newWindow({ name })),
  );
  console.log(`batch: ${count() - before} invocations`);
  await logs.panes.one().setTitle("build");

  const snapshot = await server.snapshot();
  before = count();
  const pane = snapshot.panes
    .where({ session: { is: { name: "demo" } } })
    .where({ window: { is: { name: "logs" } } })
    .where({ title: { startsWith: "bui" } })
    .one();
  if (!pane.session || !pane.window) throw new Error("pane relationships are missing");
  console.log(`${pane.session.name} / ${pane.window.name} / ${pane.title}`);
  console.log(`queries: ${count() - before} invocations`);
} finally {
  await server.kill();
}

node --import tsx batch.mts

sequential: 6 invocations
batch: 4 invocations
demo / logs / build
queries: 0 invocations

Or Bun:

bun batch.mts

Or tsx:

npx tsx batch.mts

The counts cover those window creations, after setup. The queries run in memory; take another snapshot when you want fresh tmux state. A batch runs its commands in order and stops on failure, leaving earlier changes applied.

Control mode: wait for pane output

Server.connect keeps a control observer open. subscribe gives you the events, while commands still use the server's normal executor (agent example).

Save as control.mts. Here we listen before sending the command, then capture what ended up in the pane. The printf arguments keep the completed line out of the echoed command.

import { Server } from "libtmux";

const server = new Server({
  socketName: `libtmux-control-${process.pid}`,
  configFile: "/dev/null",
});

try {
  const session = await server.newSession({
    name: "demo",
    windowName: "editor",
    shellCommand: "sh",
  });
  const live = await server.connect({ target: session.id });
  try {
    const pane = (await live.snapshot()).panes.one();
    await pane.setTitle("build");
    let output = "";
    const printed = live.subscribe().find((event) => {
      if (event.kind !== "output" || event.paneId !== pane.id) return false;
      output += event.data;
      return output.includes("build: complete");
    }, { timeoutMs: 5_000 });

    await pane.sendKeys("printf 'build: %s\\n' complete");
    if ((await printed) === undefined) throw new Error("build output never arrived");
    console.log("event: build: complete");

    const line = (await pane.capture()).find((line) => line === "build: complete");
    if (line === undefined) throw new Error("build output missing from capture");
    console.log(`capture: ${line}`);
  } finally {
    await live.close();
  }
} finally {
  await server.kill();
}

node --import tsx control.mts

event: build: complete
capture: build: complete

Or Bun:

bun control.mts

Or tsx:

npx tsx control.mts

For waiting on a change to the session itself, live.waitFor accepts a snapshot predicate. The waitFor example waits for a list of windows to exist.

Testing your own code

libtmux/testing can also replay recorded calls. Save as replay.mts. This records a snapshot, stops the tmux server, then reads it back from the recording.

import { Server } from "libtmux";
import { recordInvocations, replayInvocations } from "libtmux/testing";

const socketName = `libtmux-record-${process.pid}`;
const server = new Server({ socketName, configFile: "/dev/null" });
const recorder = recordInvocations();

try {
  await server.newSession({ name: "demo" });
  const recording = new Server({ socketName, engine: recorder.engine });
  await recording.snapshot();
} finally {
  await server.kill();
}

const replayed = new Server({ engine: replayInvocations(recorder.recording()) });
const snapshot = await replayed.snapshot();
console.log(`replayed: ${snapshot.sessions.one({ name: "demo" }).name}`);

node --import tsx replay.mts

replayed: demo

Or Bun:

bun replay.mts

Or tsx:

npx tsx replay.mts

The recording covers command calls. Control notifications still need a live tmux server (testing guide).

tmux MCP

npm · GitHub · Homepage · Guides · Tools · Resources · API Explorer

An MCP server for interacting with tmux - powered by the above core library (like libtmux-mcp!) It exposes tools for inspecting sessions, reading pane output and running commands.

Start a separate tmux server for the demo:

tmux -L libtmux-ts-mcp-demo -f /dev/null new-session -d -s demo

Use this command with a client that launches stdio MCP servers:

LIBTMUX_SOCKET=libtmux-ts-mcp-demo npx -y @libtmux/mcp

It waits for MCP requests over stdin; it is not an interactive terminal app. The guides cover connecting a client and selecting a tmux socket.

After closing the client, remove the demo server:

tmux -L libtmux-ts-mcp-demo kill-server

Workspace manager

Workspace library

npm · GitHub · Homepage · Guides · API Explorer

Build a tmux session from a configuration object with applyWorkspace. This is like tmuxp's workspace builder, but in TypeScript - and with the newer features the python version doesn't have: batching, control mode, etc.

npm install @libtmux/workspace libtmux

Save as workspace.mts. Like the core example, this uses a separate server and removes it afterward.

import { Server } from "libtmux";
import { applyWorkspace } from "@libtmux/workspace";

const server = new Server({
  socketName: `libtmux-workspace-demo-${process.pid}`,
  configFile: "/dev/null",
});

try {
  const session = await applyWorkspace(server, {
    session_name: "demo",
    windows: [
      { window_name: "shells", panes: ["printf 'hello\\n'", "pwd"] },
    ],
  });
  console.log(session.name, session.windows.at(0)?.panes.length);
} finally {
  await server.kill();
}

Run with Node:

node --import tsx workspace.mts

Or Bun:

bun workspace.mts

Or tsx, installed as above:

npx tsx workspace.mts

Each prints:

demo 2

Workspace CLI

npm · GitHub · Homepage · Guides · CLI reference · Builder API

This is like tmuxp, but in TypeScript! Load tmux workspaces from YAML or JSON, or capture a running session as a workspace file. The CLI and workspace library are separate implementations; they don't accept exactly the same configuration fields. Uses the libtmux core library.

Use a current Node 22+ release for the CLI.

npx -y @libtmux/workspace-cli --help

Save as workspace.yaml:

session_name: demo
windows:
  - window_name: shells
    layout: even-horizontal
    panes:
      - "printf 'hello\\n'"
      - pwd

Load it detached on a separate socket:

npx -y @libtmux/workspace-cli load ./workspace.yaml -L libtmux-workspace-demo -f /dev/null -d --json

Capture that session:

npx -y @libtmux/workspace-cli freeze demo -L libtmux-workspace-demo --json

Remove the demo server when finished:

tmux -L libtmux-workspace-demo kill-server

Intentions and goals

I am porting libtmux to multiple languages. TypeScript is the language I use second most after Python. Beyond that, there's also C++, Elixir, Ruby, Rust, so on. That means I can be docked fairly for spreading my attention across a large estate, meaning I may miss oppurtunities to leverage TS in the most elegant way. In those cases I'd be very grateful to get feedback how it could be done better, as the APIs aren't stable yet!

You don't need a wrapper or library to use tmux. It's a CLI app perfectly doable via command line itself thanks to its targets (UIDs that point to Session, Window, Pane) and formats. There's also control mode.

However, the tmux object graph itself is very elegant and easy to wrap one's brain around - I think it translates perfect to an OOP model. Also, things like control mode can be wrapped in a way that uses a language's unique strengths. TypeScript - being typed - made this so much better than it'd be with plain ECMAScript, having a sense of security like that is nice, Python didn't always have annotations and mypy.


r/typescript • • 9d ago

Joist 2.5 -- State Machines, PostgreJS, and Read Locks

10 Upvotes

Hello r/typescript ! Another Joist release is out with some cool new features:

https://joist-orm.io/blog/joist-2-5/

Just posting as an FYI & casual reminder that your ORM does not have to be "just a query builder". (ducks) 😅


r/typescript • • 8d ago

Galactica, a TypeScript codebase map for developers and agents

0 Upvotes

As a part of experiments exploring how we as humans fit into development processes, I built Galactica. I wanted a way to see how a codebase fits together, and to give my coding agent that same information before it starts changing things.

I rather suspected than expected this, but it also makes agents more efficient.

It follows imports and exports to build a map of the repository. The agents get neat commands for questions like "where is this defined?", "what depends on this file?" and “which tests reach it?”, I get an interactive HTML page where I can explore those same relationships.

There’s also a skill that tells the agents when to use it: get an overview before diving in, check dependents before editing, and check for new import cycles afterward.

It runs locally, doesn’t execute the code it analyses, and usually needs no configuration. It supports JavaScript too, including JSX and the script sections of Vue, Svelte and Astro files.

It's still early. I’d be interested to hear whether it helps you find your way around a repository, whether it helps you in your development processes you think it falls short. Does this answer questions you currently piece together manually, or does your existing setup already cover them?

I found it quite useful for situations where agents prototype something quickly and I want to get a good understanding of what's actually being built.


r/typescript • • 10d ago

Feeling lost with all the TypeScript options now. What are you actually using?

50 Upvotes

So the "too many JS frameworks" problem has kind of turned into "too many TypeScripts" lol. Regular TS, the Go port, the Rust ports, and whatever else shows up next week.

I've always just used plain TypeScript and never really thought about it. But now with 7 out, I keep seeing people say to just stay on 6, and honestly I don't know what to do anymore.

Is 7 actually worth upgrading to, or is it still too early? Are any of the other implementations worth looking at, or should I ignore them and stay on the boring option?

What are you all running in real projects? Curious if there are any gotchas before I touch my setup.


r/typescript • • 10d ago

What are you using for linting/formatting these days?

30 Upvotes

I've always used ESLint + Prettier, but it was getting slow and heavy, so when I found Biome I switched without thinking twice. Honestly really happy with it so far.

Lately though, Oxc (oxlint + oxfmt) has been looking pretty tempting. Has anyone made the jump, or is thinking about it?

Curious what you're running right now and why. Any gotchas, missing rules/plugins, or things you'd recommend? Especially interested in how they handle bigger codebases and type-aware linting.


r/typescript • • 10d ago

RefQL's 5th anniversary — still waiting for its first fan club

1 Upvotes

Hi,

RefQL is celebrating its 5th anniversary!

It's a TypeScript library for writing type-safe relational queries.

After 5 years, it still doesn't have many users, but I've been using it myself in production for years without problems. So at least it has one happy customer.

The idea is to make relational queries easier to write and read, without losing type safety and IntelliSense.

For example, in some ORMs you have to write things like id: true and firstName: true just to select a few fields. I wanted to keep this a bit simpler.

With RefQL you can do:

const { Player, Team } = refql.tables.public;

const readPlayers = Player([
  "id",
  "firstName",
  Team(["name"])
]);

const players = await readPlayers();

The output type is inferred automatically, also for the referenced Team.

You can also use filters, sorting, pagination, inserts, updates, deletes and raw SQL.

For PostgreSQL, RefQL can introspect the database and generate the tables and references for you.

I've also added some comparisons with Drizzle, Kysely and Prisma in the README.

GitHub: https://github.com/tureluren/refql


r/typescript • • 11d ago

Designing hardware in Typescript

9 Upvotes

Simten is an open source (https://github.com/simtenHQ/simten) HDL where circuits are written in TypeScript. The simulator is Ts as well, so everything runs in-process in Node or the browser.

I built a game around it to showcase the platform, https://play.simten.dev/

So essentially write a circuit in the editor to match the spec, it simulates live on the canvas on the right. You can toggle inputs, step the clock back/forward, drill into parts to view internals etc.

Here is the basic syntax for a half adder (GIF shows a full adder built from two of these):

import { bit, circuit } from '@simten/core'
import { And, Xor } from '@simten/core/std'

const HalfAdder = circuit('HalfAdder', {
  inputs: { a: bit, b: bit },
  outputs: { sum: bit, carry: bit },
  nodes: { xor: Xor, and: And },
  connect: ({ inputs, outputs, nodes: { xor, and } }) => [
    inputs.a.to(xor.a, and.a),
    inputs.b.to(xor.b, and.b),
    xor.out.to(outputs.sum),
    and.out.to(outputs.carry),
  ],
})

You can also run circuits headlessly in node:

import { simulate } from '@simten/core'
const sim = simulate(HalfAdder)
sim.set({ a: 1, b: 1 })
sim.read() // { sum: 0, carry: 1 }

Because it's all in-process, you can use npm packages at the transpile step. For example, this calls figlet to bake ASCII art into a ROM and streams it to a console (press play in the bottom left and change "Simten" text on line 7 ): https://simten.dev/circuit?example=figlet

For when you are sick or typing there's an mcp server, so you can ask your assistant for a circuit and it writes it, tests it and opens it for you to view. Passing it proper hardware specs I have built RISC-V cpus with it among other things. it has been a great learning experience building it. Feedback and/or contributions welcome!


r/typescript • • 11d ago

How do you bridge TypeScript DTOs and Sequelize model instances?

8 Upvotes

Good day Seniors. I'm curious how other TypeScript + Sequelize users deal with the friction between DTO Types and Sequelize model instances. A common pattern I see is simply asserting types

findAll: async () => {
  const data = await SampleModel.findAll({
    where: { status: true },
  });

  return data as unknown as SampleDTO[];
},

but at runtime it's still a Sequelize instance with dataValues, instance methods, metadata, etc., so the types don't really reflect the actual object.

you can also use { raw: true } which returns plain objects, but I've run into dialect-specific issues. For example, boolean fields stored as TINYINT may come back as 0/1 instead of being cast to true/false by Sequelize, creating another mismatch between runtime values and DTO types.

I've also experimented with:

1. Repository mapping

  return data.map(
    (item): SampleDTO => ({
      id: item.id,
      name: item.name,
      ...
      status: item.status,
    }),
  );

This gives a clean type but introduces mapper maintenance and some potential runtime overhead.

2. Zod parsing

{ SampleSchema.parse(data);}

I've also seen people run records through Zod schemas/parsers to normalize and validate the shape, but that adds another layer of processing as well.

For teams that have stayed with Sequelize (rather than Prisma, Drizzle, TypeORM, etc.), what's your preferred approach for bridging the gap between Sequelize's runtime objects and TypeScript DTOs?

Have you found a pattern that balances type safety, runtime correctness, and performance without excessive boilerplate?

I'm still learning different ORMs. Altho i had a good experience with Prisma's generated types and type casting, i'm curious how experienced Sequelize users approach this problem.


r/typescript • • 11d ago

I got a decorator to type check the generic of the class it's on and I'm way too happy about it

2 Upvotes

In my framework, every handler has to write its route twice. Once in the decorator because the runtime needs the string, and once in the generic because the types need it.

@Route('ban')
class Ban extends Handler<'kick'> {}
// no errors. cool. love that for me

That compiles. The handler for ban now thinks it's handling kick and TS is completely fine with it.

And yes, I tried only writing it once. class Ban extends Route('ban'). It worked(?), but every handler read like mixin tomfoolery, and go-to-definition took you to a generated base class instead of the real one, and I just didn't like how it looked, so I went back to writing it twice. But if I'm writing it twice, the compiler might as well make sure I wrote the same thing twice.

First problem is that a decorator only gets the class, so you need some way to read the generic back off it, and if the class doesn't use the generic, you can't:

abstract class Handler<Route extends string> {}
class Kick extends Handler<'kick'> {}

// string, not 'kick'
type Read = Kick extends Handler<infer Route> ? Route : never;

// also compiles lmao
const oops: Handler<'ban'> = new Kick();

That's structural typing, since an unused type parameter basically doesn't exist, so it has to be on a property. But I also don't want a real property on every handler, so:

declare const route: unique symbol;

abstract class Handler<Route extends string> {
    declare readonly [route]?: Route;
}

type AnyClass = abstract new (...args: never[]) => unknown;

type RouteOf<Class extends AnyClass> =
    InstanceType<Class> extends { [route]?: infer Route extends string } ? Route : never;

declare so none of this gets emitted, and it's optional so nobody ever has to set it. Now RouteOf<typeof Kick> is 'kick', and that oops line is an error as a bonus.

Also that never[] looks weird, but unknown[] doesn't work because parameters are contravariant, so unknown[] rejects any class whose constructor takes a string, and never[] accepts all of them.

Now compare the two:

type Same<Left, Right> = [Left] extends [Right] ? ([Right] extends [Left] ? true : false) : false;

You need those brackets, because without them the conditional distributes over the union, Same<'ban' | 'kick', 'ban' | 'kick'> comes out as boolean, and a perfectly correct handler gets rejected. And it has to check both ways, because one way lets @Route('ban') through on a Handler<'ban' | 'kick'>.

Now the part I like:

type Checked<Routes extends string, Class extends AnyClass> =
    Same<Routes, RouteOf<Class>> extends true
        ? Class
        : ['the decorator and the generic disagree', { decorator: Routes; generic: RouteOf<Class> }];

function Route<Routes extends string>(...routes: Routes[]) {
    // Class only shows up inside a conditional type here
    // and TS still infers it. still feels illegal
    return <Class extends AnyClass>(target: Checked<Routes, Class>): void => {
        console.log(routes, target);
    };
}

Look at target. Its type is a conditional that depends on Class, and Class is the thing TS is supposed to be inferring from target. That looks circular and you'd expect it to just break or something. But when the parameter is a conditional type, TS infers into both branches (inferToConditionalType in the checker if you want to go look), and the true branch is just Class. So Class gets inferred as the decorated class, then the conditional resolves using it, and if the routes are different the parameter turns into a tuple that a class can't be assigned to. And the message shows up right in the error:

Argument of type 'typeof TooMany' is not assignable to parameter of type
'["the decorator and the generic disagree", { decorator: "ban" | "kick"; generic: "ban"; }]'.

Wow magic. It also works with both standard and experimental decorators. Here's all of it in the playground.

Also I use this same conditional for attaching plugins, and over there it fixes an actual bug. The obvious way to write it is Class & Check<Class>, and that broke inference on generic plugin classes in both tsc and tsgo for some reason.

This is all from seedcord, a TypeScript framework that I've been building. Here's the real file, and that version also errors if the route doesn't exist. I've done a lotta TS shenanigans in this repo, so I'll prolly post about some of the other tricks too.


r/typescript • • 14d ago

Config format parser written entirely in TypeScript types

Thumbnail
github.com
5 Upvotes

r/typescript • • 14d ago

Has anyone used branded types to make a missing permission check a compile error?

13 Upvotes

Our API handlers load a record, call can(user, 'delete', doc), then call the service. Every few months someone adds a route and skips the middle step. Review misses it because the diff looks fine.

I've been playing with this:

type Authorized<T, A extends string> = T & { readonly __authorized: A };

function authorize<T, A extends string>(user: User, action: A, resource: T): Authorized<T, A> { if (!can(user, action, resource)) throw new ForbiddenError(); return resource as Authorized<T, A>; }

async function deleteDoc(doc: Authorized<Doc, 'delete'>) { ... }

Calling deleteDoc(doc) without going through authorize no longer compiles, and a doc authorized for 'edit' can't be passed where 'delete' is required.

Problems I can already see:

  • Anyone can still write as Authorized<Doc, 'delete'>, so we'd need a lint rule banning that cast outside one file.

  • The brand only proves the check passed once. If a job runs an hour later, or the user gets removed mid-request, it's stale.

  • List endpoints don't fit. I don't want to authorize 500 rows one by one; that filter belongs in the query.

  • It spreads into every service signature, and some people on the team already hate it.

Is anyone running something like this in production? Did it hold up, or did you go back to middleware plus a test that hits every route as a low-privilege user?


r/typescript • • 14d ago

Monthly Hiring Thread Who's hiring Typescript developers November

18 Upvotes

The monthly thread for people to post openings at their companies.

* Please state the job location and include the keywords REMOTE, INTERNS and/or VISA when the corresponding sort of candidate is welcome. When remote work is not an option, include ONSITE.

* Please only post if you personally are part of the hiring company—no recruiting firms or job boards **Please report recruiters or job boards**.

* Only one post per company.

* If it isn't a household name, explain what your company does. Sell it.

* Please add the company email that applications should be sent to, or the companies application web form/job posting (needless to say this should be on the company website, not a third party site).

Commenters: please don't reply to job posts to complain about something. It's off topic here.

Readers: please only email if you are personally interested in the job.

Posting BS top level comments that aren't job postings, eg "It's quiet in here" etc is a ban.


r/typescript • • 15d ago

Conditional types [Help!]

0 Upvotes

I recently released a 45-seconds YouTube video on conditional types, check my reddit profile for it. I need your feedback before I double down. Let me know what you think. Here is the script:

> Conditional types

In JavaScript, you have a conditional operator:

const ternary = typeof 'Wasim' === 'string' ? true : false 

console.log(ternary) // true

It is also known as ternary operator.

Similarly, In TypeScript, you also have a conditional operator for types:

 type Ternary = 'Wasim' extends string ? true : false // true

This is refer to as a conditional type.

Conditional types allow us to conditionally check for types that match certain criteria and decide what to do if they do match those criteria and what to do if they don’t.

Here is one example:

type Generic<T> = T extends string ? 'Wasim' : 25

type Test1 = Generic<'Hi'> // 'Wasim'

type Test2 = Generic<143> // 25

Here, we said if type argument is a subtype of string, then return type 'Wasim', if not, then return type 25.

You can return never instead of 25, if condition fails. It’s equivalent to returning nothing.

//

P.S.

I am testing content from my upcoming TypeScript book to gather early feedback.

I decided to start small with a tiny video, instead of fantasizing about releasing a value-packed longer video. It's harder to do that without any momentum. I made it quickly and decided not to obsess over scripting, editing, and fixing audio by rerecording. Those are my fun-killers. I want it to be quick, fun, and imperfect. The goal was progress, not perfection. Wish me luck!


r/typescript • • 15d ago

X3Bun a Hono/TanStack Start/tRPC/Better-Auth Turborepo Starter App

Thumbnail
github.com
0 Upvotes

This is simply a starter template that I use and I thought of sharing

  • Bun optimized

  • Front End: Tanstack Start, Tanstack Query, and Shadcn

  • Backend: Hono with tRPC

  • Authentication: Better-Auth

  • Repo Management: Turborepo

I encourage you to check and share your opinion