4

我在库中有一个代码用于进行某种事件溯源。它为每种事件定义了具有给定类型和数据的事件。

我想编写一个 reduce 函数来遍历事件列表并将它们应用于给定的初始值。

我的问题是如何定义 reducer 函数eventData参数的类型以匹配当前键?我的目标是让 TypeScript 根据对象键推断参数类型,这样我就不必对事件数据参数进行类型转换(即:)(eventData as AddEvent['data'])

简而言之:目前该wrongReducer对象会导致 TypeScript 错误,但我想定义Reducer类型以使其以这种方式工作。

是否有任何 TypeScript 魔法可以实现这一目标?

// Library code:

export interface Event<
    Type extends string = string,
    Data extends Record<string, unknown> = Record<string, unknown>
> {
    type: Type
    data: Data
}

type Reducer<T, E extends Event> = {
    [k in E['type']]: (current: T, eventData: E['data']) => T
    // What to write instead of E['data']?
}

// Example code:

type AddEvent = Event<'add', { addend: number }>
type SubstractEvent = Event<'substract', { subtrahend: number }>
type MultiplyEvent = Event<'multiply', { multiplicant: number }>

type OperatorEvent = AddEvent | SubstractEvent | MultiplyEvent

const reducer: Reducer<number, OperatorEvent> = {
    add: (current, eventData) => current + (eventData as AddEvent['data']).addend,
    substract: (current, eventData) => current - (eventData as SubstractEvent['data']).subtrahend,
    multiply: (current, eventData) => current * (eventData as MultiplyEvent['data']).multiplicant
}

/*
const wrongReducer: Reducer<number, OperatorEvent> = {
    add: (current, eventData) => current + eventData.addend,
    substract: (current, eventData) => current - eventData.subtrahend,
    multiply: (current, eventData) => current * eventData.multiplicant
}
// TSError: ⨯ Unable to compile TypeScript:
// test/so.ts:32:54 - error TS2339: Property 'addend' does not exist on type '{ addend: number; } | { subtrahend: number; } | { multiplicant: number; }'.
//   Property 'addend' does not exist on type '{ subtrahend: number; }'.

// 32     add: (current, eventData) => current + eventData.addend,
//                                                         ~~~~~~
// test/so.ts:33:60 - error TS2339: Property 'subtrahend' does not exist on type '{ addend: number; } | { subtrahend: number; } | { multiplicant: number; }'.
//   Property 'subtrahend' does not exist on type '{ addend: number; }'.

// 33     substract: (current, eventData) => current - eventData.subtrahend,
//                                                               ~~~~~~~~~~
// test/so.ts:34:59 - error TS2339: Property 'multiplicant' does not exist on type '{ addend: number; } | { subtrahend: number; } | { multiplicant: number; }'.
//   Property 'multiplicant' does not exist on type '{ addend: number; }'.

// 34     multiply: (current, eventData) => current * eventData.multiplicant
//                                                                         ~~~~~~~~~~~~
*/

const events: OperatorEvent[] = [
    { type: 'add', data: { addend: 2 } },
    { type: 'multiply', data: { multiplicant: 5 } },
    { type: 'add', data: { addend: 3 } },
    { type: 'substract', data: { subtrahend: 4 } }
]

let val = 0

for (const event of events) {
    val = reducer[event.type](val, event.data)
}

console.log(`Result: ${val}`)
// Result: 9


4

1 回答 1

2

我使用了一些创意版本的键重新映射来实现这一点:

type Reducer<T, E extends Event> = {
    [event in E as event['type']]: (current: T, eventData: event['data']) => T;
}

基本上event是指任何E可以是的,对于Reducer<any, OperatorEvent>AddEventSubstractEventMultiplyEvent

/字段中的eventData参数根据字段名称正确键入。例如for是类型:reducerwrongReducereventDataadd

在此处输入图像描述


以更经典的方式,OperatorEvent通常会被替换为“事件地图”或类似的,这更容易和更直接地映射类型。

于 2021-07-29T17:31:13.910 回答