#Infer Generic Argument from Function return

1 messages · Page 1 of 1 (latest)

west trout
#

I have the following types that I'm planning on use as the Return Type from a function

export interface ServerActionSuccess<TData> {
  error: null;
  data: TData;
}

export interface ServerActionError<TError> {
  error: TError;
  data: null;
}

export type ServerActionResult<TData, TError> =
  | ServerActionSuccess<TData>
  | ServerActionError<TError>;

Here's an example of trying to use it:

export const getTransactionsAction = async (
  data: z.infer<typeof GetTransactionsActionInput>,
): Promise<ServerActionResult> => {
  const validation = GetTransactionsActionInput.safeParse(data);
  if (!validation.success) return { error: validation.error, data: null };
  return {
    error: null,
    data: await getTransactions({ page: data.page, userId: "" }),
  };
};

The problem is that ServerActionResult takes 2 generic arguments. I'm wondering if there's any way I can make typescript infer those types for me.
Right now if I try to call this function both types data and error are infered as any:

const { data, error } = await getTransactionsAction({ page: 1 });
clear horizon
#

where would it infer from?

#

oh the function body?

#

yeah you could just omit the annotation and it'd infer the return type for you

west trout
#

I want to make this pattern the standard return for every server action that i have

clear horizon
#

there's nowhere else to infer from, unfortunately

open bison
#

you could define a more general type like:

type AnyServerActionResult =
  | ServerActionSuccess<{}>
  | ServerActionError<{}>

and then use satisfies AnyServerActionResult on returned values to check it

#

or perhaps if you have some kind of standard wrapper function around your server actions you can enforce it there

open bison
# open bison or perhaps if you have some kind of standard wrapper function around your server...

i don't really know zod, but maybe something like this?

const action =
  <Z extends z.AnyZodObject, R extends AnyServerActionResult>(
    validator: Z,
    f: (data: z.infer<Z>) => Promise<R>,
  ) =>
  (data: unknown): Promise<ServerActionError<z.ZodError> | R> => {
    const validation = validator.safeParse(data)
    if (!validation.success) {
      return Promise.resolve({ error: validation.error, data: null })
    } else {
      return f(validation.data)
    }
  }

then you define your actions like:

const getTransactionsAction = action(GetTransactionsActionInput, data =>
  getTransactions({ page: data.page, userId: '' }),
)

(that might not be totally correct but hopefully it gets the idea across)

west trout
#

I think this is getting closer to what I want

#

what bugs me is that there's libraries like Supabase JS and Zod that do stuff like the pattern I want

open bison
# west trout I think this is getting closer to what I want

you're letting the caller instantiate V with an arbitrary type there. it could be something like string or never or some other type that doesn't have any overlap with ZodError. since that function produces a specific error you need to union it into the return type

#

it's why i returned Promise<ServerActionError<z.ZodError> | R> in my example above, rather than just Promise<R>

#

with that setup you're going to have even worse problems with the T parameter. it doesn't really make sense for the caller to decide what your function returns

#
const f = <T>(): T => 42
const clearlyNotAString = f<string>()
west trout
#

a little back and forth but based on @open bison suggestions I finally settle on this:

export const protectedAction = <TArgs, TReturn>(
  action: (
    session: Session,
    args: TArgs,
  ) => Promise<ServerActionResult<TReturn>> | ServerActionResult<TReturn>,
) => {
  return async (args: TArgs): Promise<ServerActionResult<TReturn>> => {
    let session: Session;
    try {
      session = await isAuthenticated();
    } catch (e) {
      return {
        data: null,
        error: e instanceof Error ? e : new Error("Algo deu errado"),
      };
    }
    return action(session, args);
  };
};

export const publicAction = <TArgs, TReturn>(
  action: (
    args: TArgs,
  ) => Promise<ServerActionResult<TReturn>> | ServerActionResult<TReturn>,
) => {
  return async (args: TArgs): Promise<ServerActionResult<TReturn>> => {
    return action(args);
  };
};
#

publicAction just ensure that my action is returning a ServerActionResult and protectedAction injects the session