在运行时在提取的函数中提取正确的联合类型

Extract correct union type at runtime inside extraced function

我尝试为将在 xstate 中处理的事件创建类型。

当前 xstate 使用 DefinedEvent 参数调用 processChangeEvent 函数。无法更改此键入内容。
processChangeEvent 应始终使用更改事件调用,因此我想实现一种方法来检查给定事件是否为更改事件,否则会引发错误。

一切都在 if 语句中运行。打字稿编译器实现了 CHANGE 事件类型,因此我可以访问 value 属性。

export type Event<TType, TData = unknown> = { type: TType } & TData;
export type Value<TType> = { value: TType };
export type DefinedEvents = Event<'INC'> | Event<'DEC'> | Event<'CHANGE', Value<number>>;

function processChangeEventOK(event: DefinedEvents) {
    if (event.type === 'CHANGE') {
            return event.value; // NO COMPILER ERROR :-)
    } else {
        throw new Error(`Event must be of type CHANGE but is ${event.type}`);
    }
}

问题是我经常需要这个。因此,我试图提取 processEventOrThrowException 函数中的逻辑。我知道回调必须以某种方式输入不同的类型 Event<T但我不知道如何输入?

function processChangeEvent(event: DefinedEvents) {
    return processEventOrThrowException(event, 'CHANGE', (castedEvent) => {
        return castedEvent.value; // COMPILER ERROR :-(
    });
}

function processEventOrThrowException<TType>(event: Event<any>, type: string, callback: (castedEvent: Event<TType, unknown>) => any) {
    if (event.type === type) {
        return callback(event);
    } else {
        throw new Error(`Event must be of type CHANGE but is ${event.type}`);
    }
}

Link for the playground

export type PossibleTypes = 'INC' | 'DEC' | 'CHANGE';

export type Event < TType extends PossibleTypes, TData = unknown > = {
  type: TType
} & TData;
export type Value < TType > = {
  value: TType
};

export type DefinedEvents < T extends PossibleTypes = PossibleTypes > =
  T extends 'CHANGE' ?
  Event < 'CHANGE', Value < Number >>
  : Event < T >

  function processChangeEvent(event: DefinedEvents) {
    return processEventOrThrowException(event, 'CHANGE', (castedEvent) => {
      return castedEvent.value; // COMPILER ERROR :-(
    });
  }

function processEventOrThrowException < T extends PossibleTypes > (event: DefinedEvents, type: T, callback: (castedEvent: DefinedEvents < T > ) => any) {
  const isCorrectEvent = (e: DefinedEvents): e is DefinedEvents < T > => event.type === type
  if (isCorrectEvent(event)) {
    return callback(event);
  } else {
    throw new Error(`Event must be of type CHANGE but is ${event.type}`);
  }
}

说明

  • 列出所有可能的类型
  • 使用条件类型定义 DefinedEvents
  • 使用类型谓词来确定 event 是否属于正确类型

让我们定义以下助手类型和user-defined type guard function:

type EventTypes = DefinedEvents['type'];
type EventOfType<T extends EventTypes> = Extract<DefinedEvents, { type: T }>
function isEventOfType<T extends EventTypes>(
  event: DefinedEvents, 
  type: T
): event is EventOfType<T> {
  return event.type === type;
}

这会使用 DefinedEvents 区分联合并抽象检查 type 属性 的操作以区分联合成员,使用 Extract 实用程序类型。有了这些,我会这样定义 processEventOrThrowException()

function processEventOrThrowException<T extends EventTypes, R>(
  event: DefinedEvents,
  type: T,
  callback: (castedEvent: EventOfType<T>) => R
) {
  if (isEventOfType(event, type)) {
    return callback(event);
  } else {
    throw new Error(`Event must be of type ${type} but is ${event.type}`);
  }
}

这在 type 参数的类型 T 和回调 return 值的类型 R 中都是通用的。现在您的 processChangeEvent() 应该可以正常工作了:

function processChangeEvent(event: DefinedEvents) {
  return processEventOrThrowException(event, 'CHANGE', (castedEvent) => {
    return castedEvent.value
  });
}

并且由于 processEventOrThrowException() 中的 R 类型,processChangeEvent() 的 return 类型被推断为 number:

const val = processChangeEvent({ type: "CHANGE", value: Math.PI });
console.log(val.toFixed(2)) // 3.14

processChangeEvent({ type: "DEC" }); // Error: Event must be of type CHANGE but is DEC

看起来不错。

Playground link to code