elysiajs / eden

Fully type-safe Elysia client
MIT License
154 stars 37 forks source link

fix: transform entire object returned by execute #39

Closed itsyoboieltr closed 8 months ago

itsyoboieltr commented 8 months ago

This is a follow-up PR to issue #16, because the current implementation only transforms the data property of the object, and is generally not type-safe.

This PR includes a few changes depicting how I would imagine transform to ideally work: It allows the user of this library in user-land to change any property of the object returned by execute, allowing for more customisation.

The main logic works, I tested it with my use case, which is throwing an error and returning the data property only. This is very important to enable easy integration with tanstack-query.

Example code that now works:

export const app = edenTreaty<App>('http://localhost:3000', {
  transform: (response) => {
    if (response.error) throw response.error;
    return response.data;
  },
});

The only thing I have not yet got around to tackle is the type-safety part. The types in this library are pretty complex, and I am not sure where to start. I tried to look around to see where we would need to update the types, and the place for updates seems to be Sign type in src/treaty/types.ts where the Schema is mapped to the responses.

I would assume we would need to wrap this part of the code, with the return type of the given transform function. But there seems to be an array of transforms in the source code, even though we can only do one transform in user-land? Not sure what is going on there. But I guess we could iterate through the array of transforms on the type level, and get the return type of each?

The (possibly) concerned type:

Promise<
    (
        | {
              data: Route['response'] extends {
                  200: infer ReturnedType
              }
                  ? Awaited<ReturnedType>
                  : unknown
              error: null
          }
        | {
              data: null
              error: MapError<
                  Route['response']
              > extends infer Errors
                  ? IsNever<Errors> extends true
                      ? EdenFetchError<
                            number,
                            string
                        >
                      : Errors
                  : EdenFetchError<number, string>
          }
    ) & {
        status: number
        response: Response
        headers: Record<string, string>
    }
>

And here is some pseudo-code for the solution that could maybe help:

Promise<
  ApplyTransforms<
    (
        | {
              data: Route['response'] extends {
                  200: infer ReturnedType
              }
                  ? Awaited<ReturnedType>
                  : unknown
              error: null
          }
        | {
              data: null
              error: MapError<
                  Route['response']
              > extends infer Errors
                  ? IsNever<Errors> extends true
                      ? EdenFetchError<
                            number,
                            string
                        >
                      : Errors
                  : EdenFetchError<number, string>
          }
    ) & {
        status: number
        response: Response
        headers: Record<string, string>
    }
  >
>