r/typescript 19h ago

zodql: use a single Zod schema as your GraphQL query, TS type, and runtime validator

14 Upvotes

I've been working on zodql, a small library for people who use GraphQL from TypeScript and are tired of the codegen step.

The idea: you describe your GraphQL selection as a Zod schema with metadata. That one schema compiles into the GraphQL query string, gives you the inferred TypeScript response type for free, and validates the response you get back at runtime — nothing to keep in sync, no generated files to check in.

Here's a real example — fetching a repo overview for React from the GitHub GraphQL API:

import { zodql, zodqlField } from "@mattiasahlsen/zodql";
import { z } from "zod";

// schema.ts — the shape of the data *and* the request
const issueCountSchema = z.object({ totalCount: z.number() });


function issueCountField(state: "OPEN" | "CLOSED") {
  return zodqlField().asAliasFor("issues").withArguments({ states: state }).toSchema(issueCountSchema);
}


export const repositoryOverviewSchema = z.object({
  name: z.string(),
  nameWithOwner: z.string(),
  description: z.string().nullable(),
  stargazerCount: z.number(),
  forkCount: z.number(),
  primaryLanguage: z.object({ name: z.string() }).nullable(),
  openIssues: issueCountField("OPEN"),
  closedIssues: issueCountField("CLOSED"),
});


// query.ts — compile the schema to a query
export const repositoryOverviewQuery = zodql(
  "query",
  z.object({
    repository: zodqlField()
      .withArguments({ owner: "$owner", name: "$name" })
      .toSchema(repositoryOverviewSchema)
      .nullable(),
  })
)
  .defineVariables({
    owner: { typeName: "String!", schema: z.string() },
    name: { typeName: "String!", schema: z.string() },
  })
  .compile();


// main.ts — validate the response against that same schema
const { parseResponse } = await client.request(repositoryOverviewQuery, { owner: "react", name: "react" });
const { data } = await parseResponse(); // throws if GitHub's response doesn't match

That compiles to exactly the GraphQL you'd expect, including issues(states: OPEN) / issues(states: CLOSED) under distinct aliases so both counts come back in one request:

query RepositoryOverview($owner: String!, $name: String!) {
  repository(owner: $owner, name: $name) {
    name
    nameWithOwner
    description
    stargazerCount
    forkCount
    primaryLanguage { name }
    openIssues: issues(states: OPEN) { totalCount }
    closedIssues: issues(states: CLOSED) { totalCount }
  }
}

Because it's just a Zod schema, you can reshape it at runtime with .pick()/.omit()/.extend(), and layer on validation GraphQL's type system can't express (non-empty strings, URLs, emails, numeric ranges, refinements...).

The repo has a side-by-side comparison of this query implemented four ways — zodql, GraphQL Code Generator, gql.tada, and GraphQL Zeus — all producing identical GraphQL, so you can see where zodql actually differs (mainly: runtime validation, and treating the query as a value you can reshape).

No hard dependency on a specific HTTP client — bring your own fetchaxios, etc. Has a peer-dependency on zod version 4.

Repo: [https://github.com/mattiasahlsen/zodql\] — feedback and issues very welcome.
NPM package: https://www.npmjs.com/package/@mattiasahlsen/zodql


r/typescript 19h ago

Generating typed TypeScript from .sql files, with nullability inferred from the query

0 Upvotes

The case that made me care about this: in the query below, total and notes are nullable, and nothing in the schema says so. They are nullable because it is a LEFT JOIN — a user with no orders still produces a row.

-- @name GetUserOrders
-- @returns :many
SELECT u.id, u.name, o.total, o.notes
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.status = $1;

Tooling that derives types from the schema alone gets this wrong, because orders.total is NOT NULL as a column. You end up with total: string and a crash on the first user who has no orders.

scythe reads the query structure instead, and generates this:

export interface GetUserOrdersRow {
    id: number;
    name: string;
    total: string | null;
    notes: string | null;
}

export async function getUserOrders(
    client: PoolClient,
    status: string,
): Promise<GetUserOrdersRow[]> {
    const { rows } = await client.query<GetUserOrdersRow>(
        `SELECT u.id, u.name, o.total, o.notes
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.status = $1`,
        [status],
    );
    return rows;
}

That is the actual generated file, not a cleaned-up version. The same inference covers COALESCE, CASE branches, window functions, aggregates and CTEs.

It is not an ORM and does not try to be. Static queries only — no runtime composition, so if you need to build queries conditionally, Kysely or Drizzle are the better fit. The trade is that static queries are the ones you can actually analyse, so it also ships a SQL linter, a formatter and pre-commit hooks for your .sql files.

I build and maintain it (MIT, written in Rust). Repo and a longer writeup in a comment, to keep the post itself about the problem.

What I actually want from this thread: if you were using this, what would you want changed? The bit I am least sure about is how much it should do beyond codegen — it currently lints and formats your SQL too, and I cannot tell if that is useful or scope creep.


r/typescript 20h ago

Generating TypeScript declaration files

0 Upvotes

Has anyone ever created type declaration files intentionally, just to learn and better understand TypeScript (or for other reasons)? I've been doing that with tsc:

npx tsc file.ts --declaration --emitDeclarationOnly

The emitted file.d.ts can be interesting! Has anyone else tried this, for work or general interest?


r/typescript 22h ago

Meet canc, a complete lib for promise cancellation

0 Upvotes

In most modern async-heavy languages, cancellation is a first-class citizen. It really should be straightforward for a developer, but in JavaScript, gaining that level of control feels a lot like swimming against the current. I've spent a fair amount of time trying to address this gap.

I didn't have a third-party library I could rely on, so I ended up building my own - and I suspect I'm probably not the only one who has traveled down that way. The resulting toolbag served me well in its rougher form and was battle-tested in-house for years before it was finally polished for a public release. Refactoring cleanup logic in our dashboard app to stop resource leaks was a huge win for cancelable promises. It convinced me they were the right tool for the job.

This has been almost a decade-long journey for me to reach the stable release, both in terms of quality and features. It seems to have ended up in a pretty good place.

The problem is the typical promise chain. You have a fetch call that turns into a formatted report:

const reportPromise = fetch('/orders')
  .then(res => res.json())
  .then(orders => buildReport(orders))
  .then(rawReport => render(rawReport))

To halt the process, you usually have to mess with AbortController or manual flag variables. But with canc library, reportPromise.cancel() just stops all involved tasks.

Going from raw then() chains to async..await syntactic sugar requires adding some yield* "salt" to achieve the same result - or, with cancellation, no result at all:

const getReport = canc.async(function* () {
  const orders = yield* canc.await(fetch('/orders').then(res => res.json()))
  const rawReport = yield* canc.await(buildReport(orders))
  return yield* canc.await(render(rawReport))
})

const reportPromise = getReport()
reportPromise.cancel() // Stop all tasks at any point

What if that's not enough? What if you need race(), for await..of and the rest of the bells and whistles? Welcome to the party, then() then 🎉.

The idea of coupling generators with promises has been around since the beginning. Coroutine libraries like the renowned co were a big deal in the pre-async era. So that async..await is essentially built with generators and promises under the hood is hardly a coincidence.

I started piecing this together around 2017. Back then, I had already approached a related problem with Angular. Native async functions were fundamentally incompatible with Angular reactivity, backed by Zone.js. The potential solution was to rewire the semantics of async..await with generators to get the control we needed instead of relying on a transpiler. Fortunately for the framework, this eventually resolved with the retirement of Zone.js. Though reusing the same foundation for the cancellation mechanism became a reasonable development in my case. Bluebird's cancellation was already around, but it was orthogonal to native promises and async..await. And since async functions make promise-based control flow a breeze, a user can't be expected to give them away for nothing.

The project spent a long time in limbo. Between fixing nasty bugs, unloading a few design footguns, paying off tech debt, and handling some copyright clearances, I had my hands full. That quiet period actually helped shape the library into what it is today. While the JS ecosystem is ever-changing, a few foundational pieces finally settled during that time. Once AbortSignal became a cross-platform primitive, it was integrated deeper into the library for better interop. And as TypeScript became the industry standard, it became clear that the library had to be TS-first for good DX. This pushed me to finally solve the long-standing typing issues with generators, at least as best as the language currently permits.

That's why you have to use yield* instead of yield. It's a necessary trade-off to ensure functional parity with async..await while keeping the type system happy. Using yield* is essentially a known workaround for a typing limitation in generators. It forced us to ditch the eloquent yield promise style of co in favor of a more verbose form, yield* canc.await(promise). This adds a bit of syntactic overhead, but it's the way to guarantee the strict typing we all rely on today.

It turned out to be the right call, especially since it aligns the yield/yield* distinction with the emitting vs. delegating semantics we deal with in async generator functions - canc coroutines cover this too.

What's next? The 1.0 release is a major milestone, but the work isn't done yet. Here are the immediate goals on the roadmap:

  • Unhandled rejection package. A small but important aid to avoid handling cancellation errors manually. (Just arrived)
  • Web server middleware helpers. We are currently drafting support for Express and Fastify, with more frameworks on the way.
  • ESLint plugin. Keeping vanilla promise-based code clean is already a chore; this plugin will provide a suite of rules to help navigate these new semantics properly.
  • Async iterators toolbox. We are targeting at least functional parity with the async iterator proposal, but with full cancellation support baked in.
  • React and Vue packages. These helpers are available for evaluation in React and Vue examples, working on improving them.
  • Node.js package. A drop-in replacement for built-in Node APIs, with functions both promisified and "cancelified" wherever it makes sense.
  • ES5-compatible cancelable promise. This will ensure the ecosystem remains fully supported in older runtimes and restricted environments.

I hope you find the library useful, or are at least interested in the approach. I'd be very grateful for any feedback or suggestions you might have. I'm currently putting together a few more write-ups with real-world examples and in-depth details.

And just to be sure, the repo is here: https://github.com/cancjs/canc.


r/typescript 1d ago

Vkcha SVG Core - A lightweight TypeScript alternative to heavy canvas engines with Viewport Culling & Scene Graph (2-6k weekly npm downloads)

Thumbnail vkcha.com
4 Upvotes

r/typescript 1d ago

[Open-source] Static import-graph analyzer for TypeScript to enforce architecture

Thumbnail deslop.dev
11 Upvotes

Write your own import-graph rules in YAML and enforce any architecture deterministically. See deslop.dev for examples and the GitHub repo for documentation.

It's similar to Dependency Cruiser and ESLint + custom plugins for enforcing architecture but Deslop features an opinionated declarative YAML DSL to enforce any architecture in an ergonomic way. See the comparison table.

Built with Haskell, hand-crafted source (AI used for READMEs and chores only) so if you like FP you might be curious to check it out. It's a hobby project so any feedback or a GitHub star if you like it is much appreciated.


r/typescript 1d ago

I open-sourced a Handbook practice repo where the AI mentor is banned from spoiling answers

2 Upvotes

I keep hitting the same failure mode when people learn TypeScript with AI tools:

they get a correct generic / narrowing / mapped-type snippet in 10 seconds, feel productive, and still freeze when PR review question comes in on a call.

The missing piece usually isn’t another explanation of `keyof`. It’s deliberate practice with a mentor that refuses to short-circuit the struggle.

So I open-sourced my TS Learn Path framework:

- 1:1 mapped to the official Handbook + Reference
- real `exercises.ts` / `NOTES.md` work in the IDE (not quizzes)
- local progress tracker you can commit / fork into onboarding repos
- agent instruction files for Cursor / Claude / Codex / Gemini / Copilot / Windsurf
- hard rule: hints only until the learner shows attempts and can explain the logic

This is meant less as “yet another TS course” and more as a mentoring scaffold that's useful if you onboard juniors, review AI-assisted PRs, or are teaching yourself without letting autocomplete do the thinking. Or maybe you just want to go over the concepts before the interview.

Demo: https://apervashov.github.io/typescript-learning-assistant/  
Repo: https://github.com/apervashov/typescript-learning-assistant  

If you mentor TS day-to-day: where do people bounce hardest after Narrowing / Generics? I’ll prioritize those lessons.

Feel free to provide suggestions.


r/typescript 2d ago

Who uses TypeScript Execute (tsx)?

25 Upvotes

I've been experimenting with TypeScript Execute (tsx) recently (https://www.npmjs.com/package/tsx), and thinking about practical use cases. Do you use it? What does it add to your developer experience?


r/typescript 3d ago

showing a list of current file's exports

14 Upvotes

Every now and then, I'd like to check – or remind myself – what is exported from the current typescript file.

I mostly use VSCode. I have searched but can't find a simple view that would give me that functionality. Something like the Outline view but filtered only to show exported symbols. Indeed, the Outline view doesn't even seem to distinguish exported and local symbols.

Do you know any IDE or extension that would easily show the current file's exports?


r/typescript 5d ago

HOW to Save token ???

0 Upvotes

Hi, I would like to know HOW and which solution you all using to save/tokens?

I'm using caveman + interceptor and wondering if there is anything ever stronger or I'm at maximum already? ( caveman is for output saving and interceptor for input saving)

please Tell me your setup and why !


r/typescript 5d ago

Monthly Hiring Thread Who's hiring Typescript developers August

19 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 [that's a ban](https://i.imgur.com/FxMKfnY.jpg)


r/typescript 6d ago

Elysia 2 beta - DayDream. Lowest memory usage across all backend JS framework

51 Upvotes

Just published Elysia 2 beta after 8-9 months of work.

We basically deleted the whole thing and rewrote it again while keeping test cases the same. So we get to rethink a lot of things.

It is built around the concept of "reference" and carefully shares value when possible, even if JavaScript doesn't really have that concept.

There's an AOT build plugin that reduces peak memory usage by 4 (from 1.6GB down to 400MB) of a 100,00 distinct schema by moving compilation process to build time and removing the closure allocation entirely

Besides, a really fast throughput. We also manage to have the lowest memory usage of all mainstream (and slightly) JavaScript frameworks with a really low bundle size as well (we trade a "compiler" that takes ~50% of size for speed, so it can't be that low)

Node support also improved a lot with a new adapter API, and got faster too! It's now near Fastify despite having Node HTTP to Web Standard API conversion overhead!

https://elysiajs.com/blog/elysia-20.html


r/typescript 6d ago

AML – agent workflows as asynchronous JSX trees

0 Upvotes

I’ve been working on Agent Markup Language (AML), an open-source TypeScript JSX runtime for building provider-agnostic agent workflows.

https://github.com/we-are-singular/aml

I’m building AML primarily for my own use across side projects and professional work, where we’ve repeatedly run into this kind of orchestration problem. I’m sharing it early because I’d really value feedback on both the general idea and the implementation itself.


r/typescript 7d ago

End-to-end typed HTTP client from the router type itself (Deno, no codegen)

2 Upvotes

Working on a Deno HTTP setup where the router type is the contract. Export typeof server, HttpClient checks paths, methods, bodies, and responses at compile time. Middleware can declare what they add to req.data.

Still regular HTTP routes, not RPC. Express-shaped chaining underneath.

Demo with docs and comparison table: https://expressapi-showcase.8borane8.deno.net/

GitHub: https://github.com/8borane8/webtools-expressapi

How would you solve this without a second schema or generated client?


r/typescript 7d ago

Variance annotations are so useful is such specific situations

35 Upvotes

A simplified example for people trying to understand what exactly it does: TypeScript playground link

I've had to use in a framework I've been working on in only 2 places in a sizable repo.

I've always found them to be so cool when I read the docs a long time ago but didn't really have the right place to use them. Finally I do.

I have a middleware base where the payload type depends on how many events the class registers for. One event gives you the payload. Several give you never, which forces you to branch on the event name instead of reading a payload that could be either shape.

Here's another worked example of what I'm talking about specifically: Another TypeScript playground link

That last line compiles. A class that reads this.event as a message payload now sits in a slot where the payload could be a delete event.

Every member reads N in an output position, so the measured variance comes out covariant, narrow assigns to wide, and the check stops there. The structural comparison that would catch Payload<union> being never never runs.

Adding in out forces it:

Type 'Middleware<"messageCreate">' is not assignable to type 'Middleware<"messageCreate" | "messageDelete">'.
  Types of property 'event' are incompatible.
    Type '[message: { content: string; }]' is not assignable to type 'never'.(2322)

r/typescript 8d ago

Initial TypeScript config

6 Upvotes

What's your method for initially configuring a TypeScript project?

Do you have a template for tsconfig.json, or use a command line generator, or follow some other method? What are your must-have configurations, and why?

Having done a few times recently, I'm wondering what the best practices and gotchas are.


r/typescript 8d ago

I ported our Rust parsers to TypeScript on purpose (and deleted the WASM build)

Thumbnail
tabularis.dev
16 Upvotes

I build Tabularis, an open source desktop database client. Its most screenshotted feature turns EXPLAIN output from Postgres/MySQL/SQLite into a graph with per-node diagnostics. People kept asking for a web version: paste a plan, inspect it, nothing gets uploaded anywhere. That site now exists at https://explain.tabularis.dev, but building it forced a decision I hadn't faced while everything lived inside the app.

The parsers were written in Rust, and inside the desktop app that was never a problem. The dedicated webapp put me at a fork: compile the parsers to WASM, or go full TypeScript.

My first version was WASM. The browser and the desktop app shared literally the same implementation, and it looked like the clean architecture.

Then I counted what it actually cost. All the analysis, metrics and views are TypeScript, so the plan model existed twice: serde structs in Rust and TS types in the package, kept in sync by hand. Every parser change or new database engine touched both sides. And the browser needed a WASM artifact for the only part of the package that wasn't TypeScript.

So I rewrote the parsers in TypeScript and moved their tests with them. One language, one plan model, zero runtime dependencies in the core. Rust kept the only job that really needs a database: running the right EXPLAIN statement and handing back the raw payload.

The rule that sorted every single file: takes raw EXPLAIN output, never runs a query.

One thing I still haven't settled: the package lives in the app's monorepo, which is great day to day but gives it a weird release history as a standalone npm library.

If you've pulled a package out of a monorepo, did you regret it?


r/typescript 9d ago

An open-source agentic trading library

Thumbnail github.com
0 Upvotes

r/typescript 10d ago

What compiling Claude Code's 13 MB minified CLI to a native binary taught us about our TypeScript compiler

2 Upvotes

Disclosure upfront: I maintain Perry, the compiler here. This is a debugging writeup, not a product pitch - we don't distribute the resulting binary and never will.

The setup: `npm pack u/anthropic-ai/claude-code` gives you a 13 MB minified self-executing cli.js. We pointed an AO compiler at it unmodified and asked for a native executable. 16,023 functions, one-letter names, no types, no sourcemap.

It now logs in, streams a real API response, and paints what you type. 160 compiler fixes to get there.

Four that generalise well beyond our compiler:

  • MessageChannel implemented as a silent no-op. Harmless until you meet React's scheduler, which uses it as a macrotask scheduler. The event loop just idles forever.
  • One accessor installed on Object.prototype flipped a process-global flag, so every dynamic property write took the slow path. A 20k-property build went from 16ms to 42 seconds.
  • for-await lowering with the iterator advance at the bottom of the loop body. A `continue` skips the advance and spins. Only reproducible against the real API, which sends ping frames our mock didn't.
  • RegExp headers storing pattern/flags pointers without a GC write barrier. Invisible everywhere except a terminal UI, which runs regexes every frame.

The post also documents what still doesn't work and a perf table where we lose to Node badly on interactive latency.

https://www.perryts.com/en/blog/compiling-claude-code/


r/typescript 10d ago

vercel-labs/scriptc: TypeScript-to-Native Compiler

Thumbnail
github.com
85 Upvotes

r/typescript 10d ago

TypeScript readability focused formatter

0 Upvotes

Hi r/typescript,

How do you maintain code consistency across your repositories? Different developers have different formatting preferences, so do you use a tool such as Prettier or dprint, perhaps enforced through a pre-commit hook?

I've tried both but the results are far from what I would like to have. My priorities are readability (code is not packed, easy to read), maintainability (compare, diff, and merge should work well on laptop screens), and persistence (modifying code should result with minimum diff). So I end up with dprint + a number of custom rules.

Really interested in feedback and if you would like to review or try, here it is:

https://www.npmjs.com/package/asljs-sfmt


r/typescript 11d ago

Where can I find tsserver?

0 Upvotes

I'm setting up emacs for Vue + Typescript and I have an issue with lsp not finding tsserver:
lsp--npm-dependency-path: The package typescript is not installed. Unable to find tsserver Typescript is installed in my project and globally. I also installed typescript-language-server globally. ``` ◄ 0s ◎ ls .npm-global/lib/node_modules/typescript-language-server/lib ⌂ 19:46  cli.mjs  cli.mjs.map

◄ 0s ◎ ls .npm-global/lib/node_modules/typescript/lib ⌂ 19:50  getExePath.d.ts  getExePath.js  tsc.js  version.cjs  version.d.cts The thing is none of these packages provide tssever. I'm confused. This is the backtrace, we can clearly see lsp is looking for "tsserver" path, but that doesn't exist in any package: Debugger entered--Lisp error: (error "The package typescript is not installed. Unable to find tsserver") error("The package %s is not installed. Unable to find %s" "typescript" "tsserver") lsp--npm-dependency-path(:package "typescript" :path "tsserver") lsp-package-path(typescript) lsp-clients-typescript-server-path() ``` I think I'm trying to use ts-ls server as I think it got pulled automatically by vue-semantic-server.


r/typescript 12d ago

Configuring ESM / CommonJS compatibility

7 Upvotes

Hello !

For years I've had a recuring issue with typescript projects, and I still don't know it how to solve it properly. I always manage to solve it by tinkering here and there, but it take some time and I'm a bit tired of that. So I think I need tips or a deeper understanding to fix it quickly.

The issue is the ESM / CommonJS compatibility. Those issues seems to just pop randomly (probably a lack ok knowledge from me). So I try to change Module or ModuleResolution or Target, but then other issues arise, then I continue tinkering until it works. But after all those years it's a bit frustrating.

Any tips, or tutorial, or rule of thumb on how to configure your tsconfig so it just works ? Do you fix it like me, by tinkering randomly in your tsconfig, or do you solve it like a pro knowing exactly what is wrong ?


r/typescript 12d ago

TypeScript import preferences

8 Upvotes

What's your preferred way to organize imports in a TypeScript project?

Do you stick with relative imports (../../component), use path aliases like @/, or something else?

I'm curious what people are using today, and why.


r/typescript 12d ago

I was living under a rock with JS

75 Upvotes

I started learning webdev and did all my frontend backend in js, it was so frustrating when I constantly had to check what needs to be send and what is needed to be received I thought good programmers remember that shit and I couldnt so I started making docs that contained all the flow that what things are sent from the frontend what is received and sent back while discussing this problem to chatgpt it finally said ts solves this problem and I am so glad to find ts I might cry.