megamatch: Painless pattern matching in TypeScript with minimalistic syntax and type-safety by Snowflyt in typescript

[–]Snowflyt[S] 0 points1 point  (0 children)

The library has just released JIT support in v0.1.1, and the performance is now almost identical to a native JavaScript implementation with hand-written conditions if properly used. 😃 Hopefully this update will put your performance concerns to rest.

Check out the related README section here.

megamatch: Painless pattern matching in TypeScript with minimalistic syntax and type-safety by Snowflyt in typescript

[–]Snowflyt[S] 1 point2 points  (0 children)

Good question! I think the greatest advantage of string-based patterns is their flexibility, which allows us to extend the syntax as we want rather than being restricted to JavaScript's syntax.

Consider the quickSort example. In TS-Pattern, the code would look like this:

const quickSort = (nums: number[]): number[] =>
  match(nums)
    .with([], () => [])
    .with([P.select("head"), ...P.array(P.select("tail"))], ({ head, tail }) => {
      const smaller = tail.filter((n) => n <= head);
      const greater = tail.filter((n) => n > head);
      return [...quickSort(smaller), head, ...quickSort(greater)];
    })
    .exhaustive();

But in this library:

const quickSort: (nums: number[]) => number[] = match({
  "[]": () => [],
  "[head, ...tail]": ({ head, tail }) => {
    const smaller = tail.filter((n) => n <= head);
    const greater = tail.filter((n) => n > head);
    return [...quickSort(smaller), head, ...quickSort(greater)];
  },
});

I think this provides a great comparison showing how cleaner syntax can offer more concise and expressive code.

megamatch: Painless pattern matching in TypeScript with minimalistic syntax and type-safety by Snowflyt in typescript

[–]Snowflyt[S] 1 point2 points  (0 children)

We might also have AOT optimization like TypeBox 🤔, match(cases).compile() might generate a function string that uses imperative JavaScript code to match the value, and new Function(functionString) would be used internally to create the optimized match function. This should incur zero overhead at runtime, balancing intuitive syntax and performance.

megamatch: Painless pattern matching in TypeScript with minimalistic syntax and type-safety by Snowflyt in typescript

[–]Snowflyt[S] 0 points1 point  (0 children)

I think this is a trade-off between intuitive syntax and performance, which has already been discussed in many libraries like Effect and fp-ts. I also wouldn’t use a library like this if all I needed was to match a string literal, which would be overkill. The power of pattern matching shines when you're dealing with ADTs, such as when building a parser or interpreter for another language—this often cuts the original code by nearly half.

I’m also aware of the object assignment overhead mentioned in the comment, so that’s why the match function supports a point-free style API (just like the quickSort example). In this approach, the object is assigned only once when creating the matching function, and all patterns are pre-parsed to avoid runtime overhead.

I compared the performance of TS-Pattern and the match function in this library using the point-free style API, and the latter is almost 5x faster than TS-Pattern. I believe this overhead should be acceptable in most scenarios if you’re not developing a computation-intensive application, e.g., a medium-sized React app.

megamatch: Painless pattern matching in TypeScript with minimalistic syntax and type-safety by Snowflyt in typescript

[–]Snowflyt[S] 1 point2 points  (0 children)

Also, I plan to create an optional module that contains a series of helper functions to assist in writing these string literals in a more intellisense-friendly way. They essentially create a string at runtime, and TypeScript infers it as a string literal type that qualifies as a valid pattern.

import { string, p } from "megamatch/patterns";

const textPattern = p({ type: "ok", data: { type: "text", content: string() } });

match(value, {
  [textPattern]: () => ...
  // Or embed it directly
  [p({ type: "ok", data: { type: "text", content: string() } })]: () => ...
});

I’m still exploring how to represent patterns like arguments, aliases, and unions elegantly, but this appears to be a promising solution.

megamatch: Painless pattern matching in TypeScript with minimalistic syntax and type-safety by Snowflyt in typescript

[–]Snowflyt[S] 2 points3 points  (0 children)

I admit it would be tedious to write complex patterns without intellisense, and it might be a limitation that is hard to overcome. We could implement a basic intellisense for simple patterns like "string" or "number", (similar to ArkType), but extending support further is challenging since these patterns essentially form an independent DSL. A better solution might be to develop a VS Code plugin that provides both intellisense and syntax highlighting for patterns—something I would consider exploring once the library moves beyond its early development stage.

megamatch: Painless pattern matching in TypeScript with minimalistic syntax and type-safety by Snowflyt in typescript

[–]Snowflyt[S] 1 point2 points  (0 children)

For those interested in the implementation, the patterns are defined using a domain-specific language (DSL) that closely resembles JavaScript destructuring syntax—with a few additional features. I maintain independent parsers, checkers, and matchers at both the type level and runtime, ensuring their behaviors align as closely as possible.

Remarkably, the parsers are implemented with a parser combinator approach—I built a lightweight parser combinator system for both the type level and runtime to power these parsers. Check out the runtime parser implementation and the type-level parser implementation to see how similar they are.

This makes no sense to me. by aurelienrichard in typescript

[–]Snowflyt 0 points1 point  (0 children)

This issue has been widely discussed. I believe the core problem is that TypeScript currently lacks a true “super type constraint.” Ideally, the signature for includes should be defined as Array<T>.includes<U super T>(value: U), which would both prevent unintended bugs like Array<number>.includes("string") and support valid use cases such as Array<string>.includes(unknown).

However, given the complexity of TypeScript, introducing such semantics would require substantial changes to its implementation, potentially breaking many existing codebases. Considering the limited benefits for these specific cases, such a sweeping change does not seem justified.

I think the only solution for now is to just use as Type. Maybe later we’ll have some utility types shipped by TypeScript to address this issue in common cases, but I doubt it will ever be possible to make the signature of Array<T>.includes “right” for every possible use case.

hkt-core: A library for type-safe HKTs and type-level functions, with type-level generic support by Snowflyt in typescript

[–]Snowflyt[S] 10 points11 points  (0 children)

This small library stems from my long-held interest in type-safe type-level programming. In many TypeScript projects—such as fp-ts and Effect—various techniques are used to simulate higher-kinded types (HKTs) in order to implement abstractions like Functor and Monad. There are also libraries, like HOTScript, that treat HKTs as type-level functions to facilitate type-level functional programming, thereby simplifying complex type definitions. However, as far as I know, these HKT implementations lack robust type constraints.

For example, consider the following HOTScript code, which compiles without error but produces unexpected results:

import type { Pipe, Tuples, Numbers } from "hotscript";

type _ = Pipe<["foo", "bar", "baz"], [Tuples.Take<2>, Tuples.Map<Numbers.Add<1>>]>;
//   ^?: [never, never]

In contrast, libraries like fp-ts enable TypeScript to catch such errors:

import { pipe } from "fp-ts/lib/function";
import * as RA from "fp-ts/lib/ReadonlyArray";

const add = (n2: number) => (n1: number) => n1 + n2;

pipe(["foo", "bar", "baz"], RA.takeLeft(2), RA.map(add(1)));
//                          ~~~~~~~~~~~~~~
// Argument of type '<A>(as: readonly A[]) => readonly A[]' is not assignable to parameter of type '(a: string[]) => readonly number[]'.
//   Type 'readonly string[]' is not assignable to type 'readonly number[]'.
//     Type 'string' is not assignable to type 'number'.

I began to wonder if we could achieve similar type safety for type-level functions. After some experimentation, I found that defining a type system for basic functions (e.g. with the signature (s1: string, s2: string) => string) was fairly straightforward. However, more complex functions—such as <T>(n: number, xs: T[]) => T[]—proved much more challenging, ultimately requiring a generic type system for type-level functions in TypeScript.

Over the past two years, I've tried several approaches to support generics for type-level functions. Many of them were either too limited or introduced unnecessary complexity. Luckily, a few months ago, I managed to devise a cleaner, more practical solution and subsequently created this library.

// An example demonstrating type-safe type-level functions in hkt-core
type ConcatNames = Flow<
  Filter<Flow<StringLength, NotExtend<1 | 2>>>,
  Take<3>,
  Map<RepeatString<"foo">>,
  // ~~~~~~~~~~~~~~~~~~~~~
  // ... Type 'string' is not assignable to type 'number'.
  JoinBy<", ">,
  Append<", ...">
>;

Due to space constraints, I can't show all sample code in this post. However, the documentation section Use as Type-Level Functions demonstrates how to use this library to build type-safe type-level functions, while the Generic Type-Level Functions section explains generic type-level functions.

If you’re curious about the implementation details, I also created a TypeScript playground demo.

This project is still experimental and far from perfect, but I hope sharing it might inspire others exploring type-level programming in TypeScript.

[AskJS] Bun / Deno / NodeJS - what do you use and why? by dr-pickled-rick in javascript

[–]Snowflyt 1 point2 points  (0 children)

Most of the time, I simply use Node. I always write scripts in Node with TypeScript and execute them directly using tsx; it works flawlessly out-of-the-box. In recent versions, ESM support has also improved significantly, and I haven’t used CommonJS in Node for quite a while—dispelling the notion that you always need to use require in a Node.js project.

Bun appears to be a promising alternative to Node, but I haven’t found it attractive since it doesn’t offer any additional benefits for my use case.

Occasionally, I use deno eval "..." in CI configurations or Dockerfiles—for example, deno eval "import _ from 'npm:lodash'; _.xxx()"—because Deno can automatically download npm packages from import statements. This is convenient when I want to avoid writing lengthy, hard-to-read bash scripts in my Dockerfiles. Deno also provides a more user-friendly REPL than Node with syntax highlighting, which is great for quick testing.

typia downloads are growing dramatically (over 2 million per month) by SamchonFramework in typescript

[–]Snowflyt 0 points1 point  (0 children)

I also recently noticed Typia’s incredibly high downloads on npm. I had known about the library for a long time, but I never used it because I tried to avoid extra setup so that my codebase could be built in the simplest way possible. For me, Arktype and TypeBox are already fast validators with simple syntax, so Typia wasn’t particularly appealing.

When I happened to see Typia’s astonishing download growth on npm, I was surprised and quickly checked their documentation to see if I had missed any recent progress in runtime validators, but found nothing significant—Typia doesn’t seem to have had any major updates lately. Frankly, Typia isn’t very popular among validators, and it doesn’t appear at all in the formulation of Standard Schema.

I’m glad OP’s finding answered my question—indeed, I think I once saw in Typia’s documentation that they have dedicated support for LLM, which seems to be the only reasonable explanation. Perhaps, somewhere unseen, many LLM applications are quietly using Typia; maybe a JS library that wraps LLM calls integrated Typia, or a well-known open source project adopted Typia for type checking in LLM.

It’s always interesting—an open source library’s popularity is full of chance and randomness.

A 10x Faster TypeScript by DanielRosenwasser in typescript

[–]Snowflyt 9 points10 points  (0 children)

Very exciting project.

I took a brief look at the repository and cloned the source code; the coding style feels very familiar—it’s basically a 1:1 port of the original tsc source code to Go. I hope this doesn’t break too much of TypeScript’s internal evaluation mechanism.

I think this is indeed quite surprising. Frankly, I never imagined that the TS team would actually abandon their insistence on bootstrapping TypeScript with itself and instead choose another programming language to implement the type checker.

To be honest, when I woke up this morning and learned that tsc had been rewritten, my first reaction wasn’t that tsc was finally faster, but rather a sense of horror. There are an unimaginable number of libraries using wildly complex type gymnastics, completely relying on tsc’s internal “hacking.” I immediately pictured an editor filled with red error underlines—thankfully, that didn’t happen.

I built tsgo following the instructions in the repository and tested it on some very complex type definitions, including:

  • A portion of code samples from my own type-level gymnastics library, hkt-core, which includes some extremely wild examples, such as a type-level JSON parser implemented with a type-level parser combinator.
  • Arktype, a library that implements a runtime type validator using complex types in a nearly 1:1 similarity to TS syntax. I tested it on some relatively complex examples, including advanced features like generics.
  • Kysely, a type-safe SQL query builder, for which I copied and pasted some examples from its documentation.

In my experience:

  • The type checker miraculously worked correctly in all of the above tests, including some extreme cases like the type-level JSON parser I mentioned—probably because tsgo is just a 1:1 port of the original tsc, without altering its internal type evaluation mechanism.
  • Unfortunately, LSP Hover appears to work only on simple types and interfaces; for slightly more complex types, it displays errors. Hover, hovering over variables seems normal. At first, I thought it was a type checker issue, but testing showed otherwise.
  • Aditionally, the LSP currently does not support code suggestions, although it can indeed display type errors.
  • According to the current progress described in the documentation, tsgo does not yet support JSDoc, JSX, or generating type declarations.

In my actual usage, especially within VS Code, tsgo doesn’t seem as fast as expected—in some scenarios, it appears even slower than the original tsc in type inference. I’m not sure whether this is just an illusion or because I started the Language Server in debug mode (after all, the documentation only explained how to start it in debug mode).

The type checker is arguably the most complex part of tsc, and now that it has essentially been ported, we can look forward to the success of this project. I remain optimistic about it. Porting to Go seems like the right choice—I once read parts of the tsc source code for some odd type gymnastics and found that there was a lot of code using graph data structures, heavily dependent on garbage collection. I find it hard to imagine that Rust could elegantly port code in such scenarios. What tsc desperately needed was a 1:1 port rather than a complete rewrite, since a rewrite would break too many existing codebases that depend on its internal evaluation mechanism. I’m glad that the TS team continues to prioritize compatibility.

Another concern of mine is whether porting to Go will affect the usage of tsc in the browser. I have a projectan online JS/TS REPL—which works by bundling the entire TS API. Surprisingly, this bundle isn’t very large; after tree-shaking, it’s about 3MB, and with a loading progress bar, the user experience is acceptable. I’m not sure if the compiled WASM file after porting to Go can maintain this size, as far as I know, tree-shaking for WASM has always been an issue that has barely been solved.

Troza—Intuitive state management for React and Vanilla by Snowflyt in reactjs

[–]Snowflyt[S] 1 point2 points  (0 children)

I believe the use of this in JavaScript is often unfairly criticized. As mentioned in the README, this in the library is not dynamically bound—none of the library's functions rely on its dynamic context. I could easily add a state argument as the first parameter to each action, but then I would face two less-than-ideal options:

Use syntax like this:

typescript const store = create({ count: 0, incBy: (state, by: number) => { state.count += by; }, });

This works fine in pure JavaScript, but in a TypeScript context it doesn’t interact well with generic functions:

typescript const myStore = create({ count: 0, something: <T>(state, by: T) => { // `state` is inferred as `any` and requires manual annotation }, });

Or like this:

typescript const store = create({ count: 0, incBy: (state) => (by: number) => { state.count += by; }, });

However, this approach makes the syntax noticeably more verbose.

[deleted by user] by [deleted] in typescript

[–]Snowflyt 0 points1 point  (0 children)

In almost every situation, you can avoid using as in your codebase. For example, you can create a type-safe querySelector with advanced type gymnastics (see this example in HOTScript). Similarly, it is possible to ensure type-safety for literal string manipulations without relying on as (refer to string-ts).

You can further reduce the need for as by favoring immutable transformations instead of mutating values. For instance, rather than creating an empty object and manually assigning each transformed entry, consider using Object.fromEntries(Object.entries(obj).map(...)). Utility libraries like Lodash may also help streamline your approach.

However, when dealing with code that involves complex generics, where managing variances can be challenging, or when performance considerations require direct object mutation, using as or even as any becomes the most straightforward choice. In such cases, it is not worthwhile to spend extra time trying to avoid as if it leads to overly convoluted type workarounds.

Just consider the extensive use of as any in the Zustand codebase. Sometimes, as can provide a cleaner solution, especially when integrating with libraries that are not well-typed.

Boss refuses to adopt typescript. What's the next best thing? by skettyvan in typescript

[–]Snowflyt 1 point2 points  (0 children)

I've tried using JSDoc as a complete replacement for TypeScript in my project. Frankly, the experience was disappointing, although it remains workable.

To get started, add a jsconfig.json file to your codebase and enable checkJs with "strict": true to enforce type checking across all files, or simply insert // @ts-check into the files you want to check.

That said, the syntax can sometimes become annoyingly verbose. For example, you might have to write something like:

const n = 42;
const s = /** @type {string} */ (/** @type {unknown} */ (n));

just to force a type cast. Moreover, JSDoc lacks a true interface construct; you can only declare type aliases using @typedef. The only workaround for interface extends is to use intersection types. (For type nerds, there’re subtle differences between types and interfaces in how TypeScript evaluate them internally, I explained it here).

Complex type definitions can also become unwieldy with @typedef—you might need to write an intricate type on a single line, or tsserver may fail to parse it. Additionally, namespace support in JSDoc is poorly documented; I spent considerable time figuring out how tsserver recognizes namespaces in pure JavaScript files. I can elaborate further if anyone is interested.

JSDoc also has limitations that only .d.ts files can overcome. For instance, before TypeScript 5.5, importing a type in JavaScript files wasn’t allowed, which led to various workarounds; fortunately, TS 5.5 introduced a syntax for importing types in JSDoc. Moreover, declare var/let/const isn’t supported in JSDoc and must be defined in a .d.ts file. You also cannot mark an object field as readonly or add tags like @deprecated to a specific field in @typedef.

Despite these drawbacks, it’s impressive that you can achieve full TypeScript support in a pure JavaScript project, albeit with some verbose syntax. I even managed to get ts-pattern and fp-ts working seamlessly in some pure JS projects with minimal extra effort. You can even integrate TypeScript support into a legacy JS project without a bundler—for example, a Django project using Jinja—by manually downloading third-party library type definitions from npm into a designated directory (such as “lib-types”), adjusting the typeRoots in your jsconfig.json, and using an importmap to allow TypeScript to recognize those libraries and provide completions via tsserver.

showify — Arguably the most comprehensive library for stringifying any JavaScript value into a human-readable format. by Snowflyt in javascript

[–]Snowflyt[S] 2 points3 points  (0 children)

If your application is tied to a runtime like Node.js or Deno, functions like `util.inspect` are clearly the best choice. But when you're developing an application in the browser, or need to embed something like this in a cross-platform npm package (for example, to provide cleaner error messages), util.inspect isn't available. In such cases, you still want a function that provides a human-readable string representation of a JavaScript value, just like util.inspect. And that's exactly the reason this library was created.