That's a topic I really love, it makes programming with TypeScript feel magic. Unfortunately, couldn't understand really how to implement that. I'll for sure read the blog post, and I'd love to have more content on this.
I wish I saw this waaaaaaaay earlier. BTW, for the table function in the last example, you can actually use it with just "return this as QueryBuilder". This however will not work for versions less than 5.1.6. Library authors seeking to support older TS versions or projects stuck on such codebases will have to create a new object unfortunately.
This inadvertently solidified my understanding of effect.to haha,because you “run” an effect after you defined them, and they are not executed at the time they are written.
The main difference between OOP and FP for "fluent interfaces" is that you should understand that in OOP most of the time you "mutate" the instance, so there's no easy way to "clone" it. In FP, each .pipe will create a new function, so it's easy to split the same chain to 2 or more cases. The main example why "mutating" is bad is preparing `list` endpoint where you are doing COUNT() in one query, and limit/offset in another. I FP way you just add base query and splitting it to two variants: count and data. With OOP you should implement own "cloning" or create 2 queries with the same conditions
With the query builder example how would you make it show a TS error when you call more than 1 select method. And how could you limit the callable methods depending on the stage of the query. For example the only option should be the select method when you type "q." . But after you do that, then the only option should be the from() method, when you type "q.select()." ? Would be cool to see how this is done, similar to how drizzle have done it.
You would split your logic into multiple containers. You could have build the initial QueryBuilder class with only a "select" method. This select method would then return an instance of another class, lets call it SelectState. SelectState itself has only a single method as well, "from" which returns another container (FromState for example). you continue that logic and only implement the methods that you want to appear on the containers query. Through all that you propably have to pass along the previous containers or generic types from those containers. I can try to setup a minimal example and come back later.
It doesn't necessarily need to be a separate class. Another way you could go about it is by down casting your return value with 'Pick'ing the functions you want for the next chain link. You could even use the 'extends' keyword to inspect the input to return branched chains.
Doesn't this risk hurting tree shaking e.g. zod vs. valibot -- i.e. if not done carefully it can blow up client-side bundles? Either way nice video thanks for sharing your knowledge :) These interfaces do have their place!
In Rust it's pretty straightforward to create different states within the builder object so you can for example prevent (at compile time) calling select more than once in a single query (if you haven't created a subquery). How might one do that with typescript?
You make use of a state generic where the output is a transformed version of the input, but usually following the same specs (T extends {..}, U extends {..same}). By saving the literal value of what you change into the output generic, you can hide/show properties or methods in the chain. It's not that hard and writing it once makes you understand a lot more type definitions. Your returned 'this' can most easily achieve this using a helper type ToggleMethods, for example.
I think the easiest way is to represent every state as a separate type. Yes, you can construct types on the fly with Pick/Omit etc. But that makes consumer's life miserable in case they want to pass the object around and need to specify the type.
ok my comments keep getting deleted because i tried to include links :( . anyway i was linking to "The Typestate Pattern in Rust" by Cliff L. Biffle. You can search for it. What I'm realizing now is that the biggest thing that's possible in Rust but not Typescript is the ability to specify to the compiler that an object has gone out of scope or been used up or transitions to a new state. In Typescript you can correctly transform into the next "type state" which only has the correct methods to call. However the object that you transformed is still a valid object that you can transform again. This is fine for builder type patterns where the methods aren't actually doing anything until executing the "finalize" method. However, in scenarios where the method calls are actually doing work/side affects and the "type states" represent real world state being able to "revert" to a previous state can result in bugs. Does anyone have any thoughts how you might prevent this regression to a previous state via compiler errors in Typescript?
Never do this if you care about load performance and bundle size. Because you can't treeshake any of the methods on a class. Just create simple standalone functions. As reference look at the benefits of `valibot` over `zod`
Interesting point. I’m curious if there’s a way to have the best of both worlds. Perhaps a Babel-like build tool that can detect which Zod methods you actually use, and replace Zod with a stripped down version containing only those methods at runtime. I wonder if anything like this exists.
Man, does this make me feel stupid. With all the generics I am very confused. Alas. Off to go read the blog post. Maybe that will make me feel less dumb!