打字稿条件 return 基于道具的类型

Typescript conditional return type based on props

  1. 在 electron 应用程序中,我有一个组件可以呈现一个按钮并向 main 进程的任意通道发送消息。
  2. main 进程做一些工作并 return 返回结果,其 return 类型取决于频道名称。
  3. 父组件还传递一个回调和通道名称。
const Button = ({ name, channel, callback }: ButtonProps) => {
  const [isDisabled, setDisabled] = useState<boolean>(false);

  const onClickHandler = () => {
    setDisabled(true);
    window.electron.ipcRenderer.sendMessage(channel);
  };

  useEffect(() => {
    // type definition for the function below:
    // on<T>(channel: string, func: (result: T) => void): () => void;
    window.electron.ipcRenderer.on<ReturnType>(channel, (result) => {
      setDisabled(false);
      callback(result);
    });

    return () => {
      window.electron.ipcRenderer.removeAllListeners(channel);
    };
  }, [channel, callback]);

  return <button 
           type="button" 
           onClick={onClickHandler} 
           disabled={isDisabled}
         >
           {name}
         </button>;
};

export default Button;

如何指定要传递给回调的 result 类型?

我是这样开始的:

type ReturnType = OpenDialogReturnValue | SaveDialogReturnValue;

type ButtonProps = {
  name: string;
} & (
  | {
      channel: "OPEN_DIALOG";
      callback: (result: OpenDialogReturnValue) => void;
    }
  | {
      channel: "SAVE_EXAMPLE";
      callback: (result: SaveDialogReturnValue) => void;
    }
);

但是它在 callback(result);

行抛出类型错误
Argument of type 'ReturnType' is not assignable to parameter of type 'OpenDialogReturnValue & SaveDialogReturnValue'.

这里有什么问题?

有多种方法可以解决这个问题。

核心问题

您看到该错误的原因是,即使您已将 channelcallback 属性的不同值分成两个不同的对象,channelcallback 类型在 useEffect 函数内时仍未确定。这意味着 TypeScript 不知道 callback 应该有什么参数类型,并且会自动请求两者的交集 OpenDialogReturnValueSaveDialogReturnValue 类型只是为了确保无论回调在运行时是什么——它肯定会得到一个它可以使用的对象参数。

解决方案

  1. 解决此问题的最简单方法是帮助 TypeScript 并使用类型转换告诉它信任我们。正如我们所知,TypeScript 要求我们为 callback 提供一个交集类型的参数,因为它不确定哪个 callback 函数将存在于运行时 ((result: OpenDialogReturnValue) => void) | (result: SaveDialogReturnValue) => void)) 中。相反,我们可以告诉 TypeScript 只有一个 callback 并且参数类型不同 (results: OpenDialogReturnValue | SaveDialogReturnValue) => void。这可以通过首先使用名为 Parameters 的实用程序类型提取 callback 可能具有的参数类型来完成,然后将其第一个参数强制转换为 callback 以分配一个名为 [= 的新变量31=]:
useEffect(() => {
  type CallbackParameterTypes = Parameters<typeof callback>;
  const typedCallback = callback as (results: CallbackParameterTypes[0]) => void;
  window.electron.ipcRenderer.on<CallbackParameterTypes[0]>(channel, (result) => {
    setDisabled(false);
    typedCallback(result);
  });

Playground Link.

  1. 如果channelcallback参数类型之间的关联无关紧要,那么上面可以简化为:
type ButtonProps = {
  name: string;
  channel: "OPEN_DIALOG" | "SAVE_EXAMPLE";
  callback: (result: OpenDialogReturnValue | SaveDialogReturnValue) => void;
};
useEffect(() => {
  window.electron.ipcRenderer.on<Parameters<typeof callback>[0]>(channel, (result) => {
    setDisabled(false);
    callback(result);
  });

Playground Link.

  1. 解决该问题的另一种方法是使用类型谓词,这可能难以维护,但是它们让 TypeScript 确切知道何时使用什么类型。要应用它们,您首先需要将联合对象重构为单独的接口;例如OpenDialogObjectSaveExampleObject。然后,您可以使用 is 类型谓词告诉 TypeScript 当前 props 对象遵循哪个特定接口,就像在 isOpenDialog 函数中所做的那样。函数本身使用 channel 属性 来决定对象的类型。然后可以在 useEffect 中使用它来帮助确定 props.callback 在运行时将具有的参数类型。然而,正如您所观察到的那样 - 语法可能会有点重复,尤其是当您有更多类型时。为了完整性;类型谓词函数只会在传递和推断完整的 props 对象而不是单个 channelcallback 属性时产生想要的结果。这样做的原因是因为这些 属性 类型不足以确定整个对象的类型:
interface OpenDialogObject {
  channel: "OPEN_DIALOG";
  callback: (result: OpenDialogReturnValue) => void;
}

interface SaveExampleObject {
  channel: "SAVE_EXAMPLE";
  callback: (result: SaveDialogReturnValue) => void;
}

type ButtonProps = {name: string;} & (OpenDialogObject | SaveExampleObject);
if (isOpenDialog(props)) {
  window.electron.ipcRenderer.on<Parameters<typeof callback>[0]>(channel, (result) => {
    setDisabled(false);
    callback(result);
  });
} else {
  window.electron.ipcRenderer.on<Parameters<typeof callback>[0]>(channel, (result) => {
    setDisabled(false);
    callback(result);
  });
}

Playground Link.