typescript

How to create custom typed response with error code?


enum ErrorCode {
  OK = "OK",
  MAXIMUM_EXCEEDED = "MAXIMUM_EXCEEDED",
  UNKNOWN_ERROR = "UNKNOWN_ERROR",
}

type Response = {
  code: ErrorCode;
  message: string;
  data?: any;
};

type Person = {
  name: string;
  age: number;
};

type MaximumExceeded = {
  limit: number;
  inclusive: boolean;
};

export async function api(id: string, maximum: number) {
  try {
    if (maximum > 10) {
      return {
        code: ErrorCode.MAXIMUM_EXCEEDED,
        message: "maximum exceeded",
        data: {
          limit: 10,
          inclusive: false,
        },
      };
    }

    const result = await new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve([
          {
            name: "Alice",
            age: 30,
          },
          {
            name: "Bob",
            age: 31,
          },
        ]);
      }, 1000);
    });

    return {
      code: ErrorCode.OK,
      message: "success",
      data: result,
    };
  } catch (e) {
    if (e instanceof Error) {
      return {
        code: ErrorCode.UNKNOWN_ERROR,
        message: e.message,
      };
    } else {
      return {
        code: ErrorCode.UNKNOWN_ERROR,
        message: "Unknown error",
      };
    }
  }
}

async function test() {
  const result = await api("123", 20);
  if (result.code === ErrorCode.OK) {
    console.log(result.data?.name);
  } else if (result.code == ErrorCode.MAXIMUM_EXCEEDED) {
    console.log("limit", result.data?.limit);
  } else {
    console.log(result.message);
  }
}

I have some predefined error codes.

The result of the api function follows the same pattern, it alway has a code for program, a message for human and a optional data field. The promise inside is some external resources out of controll.

What I want to do is that I want to caller to get the right type by asserting on the error code. Thanks a lot.


Solution

  • You should look into Discriminated Unions, sometimes also called Tagged Unions

    enum ErrorCode {
      OK = "OK",
      MAXIMUM_EXCEEDED = "MAXIMUM_EXCEEDED",
      UNKNOWN_ERROR = "UNKNOWN_ERROR",
    }
    
    interface Response<Code, Data> {
      code: Code;
      message: string;
      data: Data;
    }
    
    type Person = {
      name: string;
      age: number;
    };
    
    type MaximumExceeded = {
      limit: number;
      inclusive: boolean;
    };
    
    type GetUserResponse =
      | Response<ErrorCode.OK, Person>
      | Response<ErrorCode.MAXIMUM_EXCEEDED, MaximumExceeded>
      | Response<ErrorCode.UNKNOWN_ERROR, unknown>;
    
    function example(response: GetUserResponse) {
      if (response.status === ErrorCode.OK) {
        // response.data is Person
        console.log(response.data.name);
      } else if (response.status === ErrorCode.MAXIMUM_EXCEEDED) {
        // response.data is MaximumExceeded
        console.log(response.data.limit);
      }
    }
    

    What this does is that, when you check for a status, typescript is able to narrow down the type of response.data within that if() block