如何为Redux风格的不相交联合事件类型添加时间戳属性?
给Redux风格事件类型添加时间戳的Flow解决方案
我明白你遇到的问题:用不相交联合类型表示Redux事件很顺手,但现在要给部分事件加必填的timestamp字段,直接用交叉类型合并却行不通。别担心,这是Flow处理联合类型和交叉类型时的常见坑,我给你几个优雅的解决方案,既能避免重复代码,又能让类型推断正常工作。
为什么直接交叉联合类型会失败?
先看你可能尝试过的代码:
/* @flow */ export type EvtA = {type: 'A', prop1: string}; export type EvtB = {type: 'B', prop2: string}; export type BaseEvent = EvtA | EvtB; // 尝试直接交叉,但类型推断会出问题 export type TimedEvent = BaseEvent & {timestamp: number};
问题出在Flow对联合类型交叉的处理上:BaseEvent & {timestamp: number}并没有被拆解成(EvtA & {timestamp: number}) | (EvtB & {timestamp: number}),而是被当作一个“混合”类型。当你尝试通过type字段收窄类型时,Flow无法正确关联type和其他属性,比如访问evt.prop1时可能会报错,因为它不确定当前事件是不是EvtA类型。
解决方案1:手动构建带时间戳的联合类型
最直接的方式是给每个基础事件类型单独加上时间戳,再组成新的联合类型:
/* @flow */ // 基础事件类型(不带时间戳) export type EvtA = {type: 'A', prop1: string}; export type EvtB = {type: 'B', prop2: string}; export type BaseEvent = EvtA | EvtB; // 定义通用的带时间戳类型 export type WithTimestamp<T> = T & {timestamp: number}; // 构建带时间戳的事件联合类型 export type TimedEvent = WithTimestamp<EvtA> | WithTimestamp<EvtB>;
这样Flow能清晰识别每个带时间戳的事件类型,类型收窄也能正常工作:
// 创建带时间戳的事件 const createTimedEvent = <T: BaseEvent>(event: T): WithTimestamp<T> => { return { ...event, timestamp: Date.now() }; }; // 处理事件时的类型推断完全正常 const handleTimedEvent = (evt: TimedEvent) => { switch (evt.type) { case 'A': // Flow自动推断evt是WithTimestamp<EvtA>,可以安全访问prop1和timestamp console.log(`Event A: ${evt.prop1} at ${evt.timestamp}`); break; case 'B': console.log(`Event B: ${evt.prop2} at ${evt.timestamp}`); break; } };
解决方案2:用条件类型自动分发联合类型
如果你的事件类型很多,不想一个个手动包裹WithTimestamp,可以利用Flow的条件类型自动处理联合类型的每个成员:
/* @flow */ export type EvtA = {type: 'A', prop1: string}; export type EvtB = {type: 'B', prop2: string}; export type BaseEvent = EvtA | EvtB; // 条件类型会自动遍历联合类型的每个成员 export type WithTimestamp<T> = T extends {} ? T & {timestamp: number} : T; // 直接生成带时间戳的联合类型,无需手动列举每个事件 export type TimedEvent = WithTimestamp<BaseEvent>;
这个方式和方案1效果完全一样,但代码更简洁,新增事件类型时只需要更新BaseEvent,TimedEvent会自动同步。
额外技巧:区分带/不带时间戳的事件
如果你的应用里同时存在带时间戳和不带时间戳的事件,可以把它们分成两个联合类型,方便后续使用:
/* @flow */ // 基础事件 export type EvtA = {type: 'A', prop1: string}; export type EvtB = {type: 'B', prop2: string}; export type EvtC = {type: 'C', prop3: boolean}; // 不需要时间戳的事件 // 分类联合类型 export type BaseEvent = EvtA | EvtB | EvtC; export type TimedBaseEvent = EvtA | EvtB; // 需要时间戳的子集 export type TimedEvent = WithTimestamp<TimedBaseEvent>;
这样你可以在需要的地方分别使用BaseEvent和TimedEvent,类型系统会帮你严格区分。
内容的提问来源于stack exchange,提问作者Ken Arnold
相关产品推荐
相关产品推荐

