Discriminated Union - Typescript

In computer science, a tagged union, also called a variant, variant record, choice type, discriminated union, disjoint union, sum type or coproduct, is a data structure used to hold a value that could take on several different, but fixed, types. (wikipedia)

Lets see how this type variance could be useful when converting the code below from JavaScript to TypeScript

  class Scenario {
  async getMetadata() {
    try {
      return {
        success: true,
        response: "some response here",
      };
    } catch (err) {
      //log the error and return
      return {
        success: false,
        errorMessage: "some message here",
      };
    }
  }

  async process() {
    const response = await this.getMetadata();
    if (response.success === true) {
      //more code here..
    } else {
      //even more code here..
    }
  }
}
  

Same code in Typescript, this is a common approach

  // Result type

type Result<T> = {
  success: boolean;
  response?: T; //in case of error response wont be there
  errorMessage?: string; //in case of response error wont be there
};

declare function apiCall(): Promise<string>;

class Scenario {
  async getMetadata<U>(): Promise<Result<U>> {
    try {
      const data = await apiCall();

      return {
        success: true,
        response: data as U,
      };
    } catch (err) {
      //log the error

      return {
        success: false,
        errorMessage: "some message here",
      };
    }
  }

  async process() {
    const result = await this.getMetadata<string>();

    //additional check here

    if (result.success === true && result.response) {
      //more code here..
    } else {
      //even more code here..
    }
  }
}
  

The issue with above approach is that both response and errorMessage are optional and would need an additional check before accessing, this is because of 2 reasons:

  • In case of error response wont be there, so has to be optional

  • Similarly, in case of response, errorMessage wont be threre, again has to be optional

Although not bad, but could be improved using a discriminated union, here's the snippet below, first we convert Result<T> to a discriminated union, we will use success as literal type to discriminate the Result

  
    type SuccessResult<T> = {
        success:true,
        response:T
    }
    type ErrorResult = {
        success:false,
        errorMessage:string
    }
  
      type Result<T> = SuccessResult<T> | ErrorResult;
  

Now the code could be re-written without an additional check.

  
class Scenario {
  async getMetadata<U>(): Promise<Result<U>> {
    try {
      const data = await apiCall();

      return {
        success: true,
        response: data as U,
      };
    } catch (err) {
      //log the error
      return {
        success: false,
        errorMessage: "some message here",
      };
    }
  }

  async process() {
    const result = await this.getMetadata<string>();

    if (result.success === true) {
     // This will be infered as SuccessResult
    } else {
      //even more code here..
    }
  }
}
  

Hope this helps in understanding discriminated union in TypeScript.