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

问题描述

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

当前,xstate使用processChangeEvent参数调用DefinedEvent函数。此类型无法更改。
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}`);
    }
}

解决方法

让我们定义以下助手类型和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所区分的联合,并使用Extract实用程序类型来抽象检查type属性以区分联合成员的操作。有了这些,我将像这样定义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}`);
  }
}

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

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

并且由于R中的processEventOrThrowException()类型,因此推断processChangeEvent()的返回类型为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

,

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,(castedEvent) => {
      return castedEvent.value; // COMPILER ERROR :-(
    });
  }

function processEventOrThrowException < T extends PossibleTypes > (event: DefinedEvents,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是否为正确的类型