Hacker News new | ask | show | jobs
by fullstackchris 1401 days ago
Do you have an example? I've seen TypeScript handle crazy nested types (and generics) with ease.
2 comments

Perhaps the React-Query source itself is a good example of when "simple" is still not "easy to read/write":

I picked out this file pretty arbitrarily: https://github.com/TanStack/query/blob/main/packages/react-q...

The author of react-query seems to be very clear at communicating how the library works and the library itself provides a great developer experience, but it's also an example of how much work can go into correct typing in library code.

I recently typed my state for zustand (react library for managing state) and it was a horrible experience.

An example from the docs with middleware:

https://docs.pmnd.rs/zustand/typescript#middleware-that-chan...

If anyone calls this “easy, just learn generics” I won’t believe them further.

Honestly, if that's what it takes to make something work with Typescript I'm surprised anyone uses it. I thought Scala/Cats was the only language/cult that indulged typism to this extent. **d save us from the static typing Spanish Inquisition and bring back dynamic languages.
Seeing one the worst implementation of static typing and then blaming static typing as a whole is unfair. gradual typing is just an abomination in practice as can be seen in both TS and Python. Maybe a language that's build from the start with gradual typing in mind can make it good but I doubt it. Static and dynamic typing are just at odds in general.
The types in Typescript are amazing. I believe they’re turing complete? The problem is that this allows people to do many really complicated things that’d just be avoided or impossible in other languages.

Typescript is a horrible as you make it.

Yeah, this is the sentiment I keep feeling looking at most of the examples being provided in this thread and the article. A lot of the things people are doing here aren't even possible in most static typed object oriented languages or are hard to do and generally discouraged, and violate things like the Liskov Substitution Principle and Open/Closed Principle (especially the Open/Closed Principle; many of these examples are redux adjacent and a lot of them require modifying existing State object instances rather than extending them at a class level per the principle).

Many of these things are easy to do in JS because JS wasn't built to be static typed object oriented language. Many of these are hard to type because the underlying language is so permissive.

As library authors the desire is to be as permissive as possible, to use the "simplicity" of the untyped JS language to express an API surface that accepts any combination of possible inputs and does the greatest amount of work with that. There are many "JS native" libraries that do that, including JQuery's $ "operator" that was a swiss army knife of a million different tasks all using a single constructor which created object instances that any number of plugins mutated over time. As a JQuery user that was a very easy experience to work with, it's permissivity felt like simplicity and easy-to-learn. As someone who briefly spent time debugging JQuery types definitions in Typescript, that was an incredible nightmare. (That was also many, many versions of Typescript back with fewer typing tools, many of which would have helped a lot, but also made everything even more complex than it was at the time.)

I do feel like a lot of library authors sometimes need to ask themselves as types grow more complicated in their libraries if the trade-offs are worth it. That "simple" API they are trying to give their users, is there a more "complicated" API with simpler types to use instead? Sometimes that's actually the simpler API to use understand too, but many of your end users see your types indirectly and simpler types in the API are simpler experiences of those types.

The article mentions needing to add lots of overloads and that's specifically something I'm thinking about there. Another example in this thread included a function that could take a set of arguments as either an object ({ a?: thing, b?: someOtherThing, c?: thirdThing }) or tuple ([a, b, c]: [thing, someOtherThing?, thirdThing?]) and in both cases many of the parts were optional, complicated further by the thing type being generic itself and the tuple accepting different orders of parameters. Can you just pick one, object or tuple for your API? Users have "less freedom" to do as they wish and the result seems less "simple", but you can eliminate so much complexity in your types. If you do still need both ways, maybe it's reasonable to split from "overloads" to different functions setThingsObject(things: { a?: thing, b?: someOtherThing, c?: thirdThing }) versus setThingsQuickly(things: [thing?, someOtherThing?, thirdThing? ]).

It's easy to armchair quarterback refactor other people's APIs, of course, but it's certainly a factor when I'm building my own APIs: These types are starting to get complicated here, should I refactor to simpler types? Should this API be split into two different/distinct endpoints to simplify the type signature? And so forth.

Some complexity is unavoidable, of course, but sometimes it is worth trading "simple, permissive JS with incredibly complex types" for "tedious explosion of JS that looks complex at first glance with incredibly simple types". It's a deep trade-off space to explore and what's right for any individual project is deeply personal opinion.

We use Rematch/Redux for state management and typing it[1] was pretty easy.

I think it really depends.

[1] https://rematchjs.org/docs/getting-started/typescript

That is terrible. Good luck onboarding new developers or junior team members into your project.

I get playing with technology for side projects, but for something commercial there is no way I would sign off on including that package into a project.

Did you miss this example from the linked post about redux-toolkit? https://github.com/reduxjs/redux-toolkit/blob/4ab8c42cb20ae1...

This is what types look like when you're on the library side. The post made very clear they aren't talking about the app dev side. The library types are complex like this so that the app devs have a smoother experience.

Might help understanding if the author used ‘meaningful’ variable names…
It would be more understandable if you didn’t use single letter type names (someone posted an example from react-query elsewhere)
Those are some huge ternary ifs

I really wish Typescript had a great pattern matching system. It would be very nice indeed for structural typing, everything would be easier to read, probably some ways to add more power/expressivity, etc.

eg

    type GetOptions<T> =
      // Part 1: responsible for applying explicit type parameter to function arguments, if object { queryFnData: TQueryFnData, error: TError, data: TData }
      T extends {
        queryFnData: infer TQueryFnData
        error?: infer TError
        data: infer TData
      }
        ? UseQueryOptionsForUseQueries<TQueryFnData, TError, TData>
        : T extends { queryFnData: infer TQueryFnData; error?: infer TError }
        ? UseQueryOptionsForUseQueries<TQueryFnData, TError>
        : T extends { data: infer TData; error?: infer TError }
        ? UseQueryOptionsForUseQueries<unknown, TError, TData>
        : // Part 2: responsible for applying explicit type parameter to function arguments, if tuple [TQueryFnData, TError, TData]
        T extends [infer TQueryFnData, infer TError, infer TData]
        // ...
becomes

    type GetOptions<T> =
      // Part 1: responsible for applying explicit type parameter to function arguments, if object { queryFnData: TQueryFnData, error: TError, data: TData }
      T extends match { 
          { queryFnData: infer TQueryFnData, error?: infer TError, data: infer TData } => UseQueryOptionsForUseQueries<TQueryFnData, TError, TData>
          { queryFnData: infer TQueryFnData; error?: infer TError } => UseQueryOptionsForUseQueries<TQueryFnData, TError>,
          { data: infer TData; error?: infer TError }               => UseQueryOptionsForUseQueries<unknown, TError, TData>,
          [infer TQueryFnData, infer TError, infer TData]           => UseQueryOptionsForUseQueries<TQueryFnData, TError, TData>
          [infer TQueryFnData, infer TError]                        => UseQueryOptionsForUseQueries<TQueryFnData, TError>
          // ...
      }
or even, to make the top level way easy to understand in this case:

    type GetOptions<T> =
      T extends match { 
          Part1 => Part1Match
          Part2 => Part2Match
          Part3 => Part3Match
      }
where Part1Match etc are themselves pattern matches, so you can compose them like functions.
IME it's precisely the expressivity of nested types and generics and variations thereof that makes it hard for the library author.

The expressivity makes it possible for library authors have IntelliSense give more useful suggestions to the consumer, make automatic inference work in more places for the consumer, and make more illegal usage be impossible, at the expense of complicated type constructs. But of course all these is somewhat optional.