如何更优雅地缩小TypeScript类型,而不使用两个返回?

s3fp2yjn  于 2022-12-14  发布在  TypeScript
关注(0)|答案(2)|浏览(124)

假设我有一个基于下面定义的类型的对象ApiErrorType | ApiSuccessType,这两个类型有一些相似之处,但是timestamp只与ApiErrorType相关,如果isErrorfalse,则timestamp永远不会出现。
我想通过一个返回MyErrorType | MySuccessType的格式化函数将这个对象与union类型放在一起。重要的是,下游代码可以根据isError的值知道它正在处理这两种类型中的一种。
我能想到的唯一方法是让我的格式化函数有两个单独的返回语句,并创建局部变量以避免应用于两个返回的冗余代码。
有没有更优雅的方法来实现这个功能?更类似于betterFormatter函数,但实际上它并不起作用?

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);
}
zsohkypk

zsohkypk1#

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>;
};

这里有一种方法可以用单个返回值来实现这一点,但我认为它的可读性较低。

k3bvogb1

k3bvogb12#

您可以使用区分联合类型来更好地定义您的API响应:

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
}

如果你想玩它:
typescript Playground链接

相关问题