11 votes

FizzBuzz as a TypeScript type

9 comments

  1. [8]
    skybrian
    Link
    It’s just for fun, but the explanation of how conditional types work might be useful. (I have never used a conditional type in a real program, though.)

    It’s just for fun, but the explanation of how conditional types work might be useful.

    (I have never used a conditional type in a real program, though.)

    3 votes
    1. [7]
      teaearlgraycold
      Link Parent
      I’ve used them pretty frequently in a system meant to substitute for code generation. A number of other developers that worked with the system I built seemed to like it.

      I’ve used them pretty frequently in a system meant to substitute for code generation. A number of other developers that worked with the system I built seemed to like it.

      1. [6]
        skybrian
        Link Parent
        Interesting. I’m imagining an interpreter. What sort of things do you do?

        Interesting. I’m imagining an interpreter. What sort of things do you do?

        1 vote
        1. [5]
          teaearlgraycold
          Link Parent
          There's this type that I have used in many projects. /** * Converts a type into its post JSON serialization/de-serialization version. */ type JSONSerialized<T> = // We first check if we're looking...

          There's this type that I have used in many projects.

          /**
           * Converts a type into its post JSON serialization/de-serialization version.
           */
          type JSONSerialized<T> =
            // We first check if we're looking at a primitive type. Primitives will be passed
            // through untouched by `JSON.parse(JSON.stringify(...))`, so `JSONSerialized` replicates
            // that behavior. The exact `T` type is output instead of `number` or `string` in case
            // the type is actually a union (ex: "foo" | "bar"). This way the union is preserved.
            T extends number ? T :
            T extends string ? T :
            T extends boolean ? T :
            T extends null ? null :
            // If we aren't looking at a primitive, then we'll next check if we're working with an
            // array. If so we convert the array's element type using a recursive type call.
            T extends (infer Elem)[] ? JSONSerialized<Elem>[] :
            // Here's the magic that takes care of `Date`s. Some types have a `toJSON` function.
            // `JSON.stringify` will call this function if it doesn't already know what to do with
            // a value. By using `infer` we can dynamically extract the return type of a `toJSON()`
            // as defined on ANY type - including `Date.toJSON`.
            T extends { toJSON: () => infer Return } ? Return :
            // Lastly, check if we're working with an Object and if so, iterate through its
            // key/value pairs - sending each value into a recursive call to `JSONSerialized`.
            T extends { [Key in keyof T]: T[Key] } ? { [Key in keyof T]: JSONSerialized<T[Key]> } :
            never;
          

          It's employed when importing HTTP response types from the back end code into the front end code. If your back end function returns something like:

          export function handler() {
            return { timestamp: new Date() };
          }
          

          You can't cast the HTTP response's natural any type to ReturnType<typeof handler> as that will be incorrect. After this has gone through JSON.stringify() -> JSON.parse() the timestamp field is an ISO formatted string, not a Date. So the type above walks through any response type and converts it to how it will look in the front end. This is used in concert with other TypeScript code which combined eliminates the need for something like GraphQL for HTTP client code generation. It's still REST - so not a true replacement for GraphQL. Although I prefer it that way.

          1 vote
          1. [4]
            skybrian
            Link Parent
            Very cool! It seems like, by casting the result of JSON.parse() in this way, you're not verifying what's coming in on the wire? That is, it assumes that the front end and back end are synced to...

            Very cool!

            It seems like, by casting the result of JSON.parse() in this way, you're not verifying what's coming in on the wire? That is, it assumes that the front end and back end are synced to the same version of the schema. Maybe you check that some other way?

            I use Zod and pay the performance penalty for verifying incoming data, which explains why this didn't come up for me.

            1 vote
            1. [3]
              teaearlgraycold
              Link Parent
              Correct, nothing is verified. But the same thing can be said about your API server's interactions with the database. It's a problem that is handled well enough within applications you own both...

              Correct, nothing is verified. But the same thing can be said about your API server's interactions with the database. It's a problem that is handled well enough within applications you own both sides of by being mindful of schema changes. That won't scale up to a ton of devs - but I haven't used this for such a situation. For what it's worth I haven't been bitten by this shortcoming. Any ephemeral synchronization issues will manifest in pretty much the same way as when using Zod. The user will see an error or a form won't submit. They refresh the page, the client updates, and then it works.

              A Zod schema in a shared library - imported by the front and back end - would also be a pretty good solution.

              1. [2]
                skybrian
                Link Parent
                Yes, usually reloading the page isn't that big a deal for sophisticated users, though it's annoying to lose work. (On Tildes, I lose work fairly often on phone and tablet, because visiting a heavy...

                Yes, usually reloading the page isn't that big a deal for sophisticated users, though it's annoying to lose work. (On Tildes, I lose work fairly often on phone and tablet, because visiting a heavy web page in a different tab will evict the Tildes tab from memory.)

                But for some users, every time something doesn't work, it's confusing, so it's good to at least have a decent error message explaining what to do.

                1. teaearlgraycold
                  Link Parent
                  My bad. The server side does verify (because if it didn't that would be terribly insecure). The same source of truth for the POST body verification is used for the type-level API interface...

                  My bad. The server side does verify (because if it didn't that would be terribly insecure). The same source of truth for the POST body verification is used for the type-level API interface validation. And when the body/query params fail to validate there is a specific signal for that which can be handled in forms.

                  But when the client reads data it does no verification and will eventually hit some kind of type error if the schemas are out of sync.

                  1 vote
  2. krellor
    Link
    This is really fun, thanks for sharing. The logic and the implementation of more complex operators with basic elements gives me a sort of deja vu from past projects designing ALU's out of single...

    This is really fun, thanks for sharing. The logic and the implementation of more complex operators with basic elements gives me a sort of deja vu from past projects designing ALU's out of single logic gates, like the NAND gate. It's fun to see the same types of ideas being used in really fun, different ways.

    2 votes