在 TypeScript 中使用索引签名时如何将值类型与键匹配?

How to match value types to keys when using index signatures in TypeScript?

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

我想编写一个 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


我使用 key remapping:

的一些创意版本来实现它
type Reducer<T, E extends Event> = {
    [event in E as event['type']]: (current: T, eventData: event['data']) => T;
}

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

reducer/wrongReducer 字段中的 eventData 参数已根据字段名称正确键入。例如。 eventData 对于 add 是类型:


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