encore icon indicating copy to clipboard operation
encore copied to clipboard

Issue with TypeScript union types causing error in `encore run`

Open ebalo55 opened this issue 6 months ago • 3 comments

The run command does not accept union types or anything that is not an interface as the return type of any API call.

Here is the error

error: expected named interface type, found Union(Union { types: [Named(Named { obj: Object { name: Some("LoginSuccessfulResponse") }, type_arguments: [] }), Named(Named { obj: Object { name: Some("MFARequiredResponse") }, type_arguments: [] })] })
  --> [...]\api\src\auth\login.ts:57:34
   |
57 |     async (params: LoginParams): Promise<LoginResponse> => {
   |                                  ^^^^^^^^^^^^^^^^^^^^^^

Sample code to reproduce

interface LoginParams {
    coin: bool
}

export interface LoginSuccessfulResponse {
    ok: true;
    param1: string
}

interface MFARequiredResponse {
    ok: false;
    param2: string
}

type LoginResponse = LoginSuccessfulResponse | MFARequiredResponse;

export const login = api(
    {
        method: [ "POST" ],
        expose: true,
        path:   "/auth/login",
    },
    async (params: LoginParams): Promise<LoginResponse> => {
        if (params.coin) {
            return { ok: true, param1: "true" }
        }
        return { ok: false, param2: "false" }
    }
);

ebalo55 avatar May 11 '25 15:05 ebalo55

Union types in the root definition for the response is not supported but as a workaround I think both fields could fit as null values in a single interface.

luisnquin avatar May 11 '25 15:05 luisnquin

yes, they can, the point here is that using that kind of type union allows to disambiguate the calling result based on the ok value, so that

// in client
response = call_ep();

if(response.ok) {
  // here response has access to param1 only, other fields are considered non existent
  return;
}

// here response instead can access param2 without other checks

a syntax like the one before avoids the necessity to bloat the code with something like const test = response.param1 || response.param2 || ""

ebalo55 avatar May 12 '25 13:05 ebalo55

You can use unions, just not on the root level, so you could do something like:

export type LoginSuccess = {
  ok: true;
  param1: string;
};

export type LoginFailure = {
  ok: false;
  param2: string;
};

export interface LoginResponse {
  status: LoginSuccess | LoginFailure;
};

fredr avatar May 13 '25 08:05 fredr