Home > Blockchain >  How to narrow TypeScript types more elegantly, without using two returns?
How to narrow TypeScript types more elegantly, without using two returns?

Time:12-14

Imagine I have an object which is ApiErrorType | ApiSuccessType based on the types defined below. These two types have some similarities, but timestamp is only relevant to the ApiErrorType and will never be present if isError is false.

I want to put this object with the union type through a formatting function that returns MyErrorType | MySuccessType. It's important that downstream code can know it's dealing with one of these two types, based on the value of isError.

The only way I can figure how to accomplish this is for my formatting function to have two separate return statements, and to create local variables to avoid redundant code which applies to both returns.

Is there some more elegant way to achieve this? More similar to the betterFormatter function, which doesn't actually work?

interface Base {
    readonly isError: boolean;
    readonly timestamp?: number;
    readonly msg: string;
}

interface ApiErrorType extends Base {
    readonly isError: true
    readonly timestamp: number;
}

interface ApiSuccessType extends Base {
    readonly isError: false;
    readonly timestamp: undefined;
}

const errorObj = {
    isError: true,
    timestamp: 787149920,
    msg: "errored"
} as const;

const successObj = {
    isError: false,
    timestamp: undefined,
    msg: "succeeded"
} as const;

interface MyErrorType {
    readonly isError: true;
    readonly date: Date;
    readonly msg: string;
}

interface MySuccessType {
    readonly isError: false;
    readonly date: undefined;
    readonly msg: string;
}

const formatter = (obj: ApiErrorType | ApiSuccessType): MyErrorType | MySuccessType => {
    // This version works, but it requires two separate returns, and requires `msg` to be 
    // assigned to a local variable to be reused in both places. Overall, it's repetative.
    const msg = obj.msg.toUpperCase();

    if (obj.isError) {
        return {
            isError: obj.isError,
            date: new Date(obj.timestamp),
            msg
        }
    }

    return {
        isError: obj.isError,
        date: undefined,
        msg
    };
};

// TODO: How could I accomplish this?
const betterFormatter (obj: ApiErrorType | ApiSuccessType): MyErrorType | MySuccessType => {
  // This version should have one return, like the below, but I should somehow be able to
  // tell TypeScript that the return type is in face `MyErrorType | MySuccessType`.
  // As written, `betterFormatter` does not work.
  return {
    isError: obj.isError,
    date: obj.timestamp ? new Date(obj.timestamp) : undefined,
    msg: obj.msg.toUpperCase()
  };
}

const error = formatter(errorObj);
const success = formatter(successObj);

if (error.isError) {
    // We know this is a date
    error.date.getDate();
}

if (!success.isError) {
    // We know this is undefined
   console.log(success.date === undefined);
}

CodePudding user response:

type MyReturnType<T extends ApiErrorType | ApiSuccessType> =
  T extends ApiErrorType ? MyErrorType : MySuccessType;

const betterFormatter = <T extends ApiErrorType | ApiSuccessType>(
  obj: T
): MyReturnType<T> => {
  return {
    isError: obj.isError,
    date: obj.isError ? new Date(obj.timestamp) : undefined,
    msg: obj.msg,
  } as MyReturnType<T>;
};

Here is one way to do this with a single return, but I would argue it is less readable.

CodePudding user response:

You can use discriminate union type to define better your API response:

type APIResponse = 
   | { 
       isError: true;
       timestamp: Date;
       msg: string
     }
   | {
       isError: false;
       msg: string
     }

type MyErrorType = Extract<APIResponse, {isError: true}>
type MySuccessType = Extract<APIResponse, {isError: false}>

function formatter(obj: APIResponse): MyErrorType | MySuccessType {
  if (obj.isError) {
     return {
      ...obj,
      timestamp: new Date(obj.timestamp),
    }
  }
  return obj as MySuccessType // here there is no need to cast it because TS already know that obj is of type MySuccessType and it won't have the timestamp property
}

If you want to play with it:

Typescript Playground Link

  • Related