为什么Partial是非常有用的TypeScript函数? -Event-Driven


借助reduce函数,它可以做魔术:能在事件溯源EventSourcing中聚合流(事件流)构建当前的聚合状态。
首先定义事件类型和聚合数据。我使用电影票预订作为示例用例:

interface SeatReserved {
    eventType: 'SeatReserved';
    reservationId: string;
    movieId: string;
    seatId: string;
    userId: string;
}

interface SeatChanged {
    eventType: 'SeatChanged';
    reservationId: string;
    newSeatId: string;
}

type ReservationEvents = SeatReserved | SeatChanged;

interface Reservation {
    reservationId: string;
    movieId: string;
    seatId: string;
    userId: string;
}

订票时可以保留座位并进行更改。重要的是两个事件都具有带有硬编码事件类型名称的eventType属性。我们稍后再使用。
在事件溯源中,事件在逻辑上被分组为流。流是实体的表达。在实体上进行的每个业务操作都应作为持久事件结束。
实体状态是通过读取所有事件并按出现顺序逐一应用它们来检索的。我们正在将事件集转换为单个实体。这是reduce函数的主要作用。它在每个数组元素上执行reducer函数(可以提供),从而产生单个输出值。
有3件事情要注意:
  • TypeScript中的reduce是一种通用方法。它允许提供结果类型作为参数。它不必与数组元素的类型相同。
  • 您还可以使用可选参数来提供默认值以进行累加。
  • 使用Partial <Type>作为通用的reduce参数。它构造一个将类型的所有属性设置为可选optional的类型。它将返回一个代表指定类型的所有子集的类型。这是非常重要的,因为TypeScript会强制您定义所有必需的属性。我们将聚合状态的不同状态合并为一个最终状态。只有第一个事件(SeatReserved)需要提供所有必填属性字段。其他事件将仅进行属性字段的部分更新(SeatChanged仅更改seatId)。

让我们看看它在实践中是如何工作的:

var events: ReservationEvents[] = [
    {
        eventType: 'SeatReserved',
        reservationId: 'res-homeAlone-1',
        movieId: 'homeAlone',
        seatId: '44',
        userId: 'ms_smith',
    },
    {
        eventType: 'SeatChanged',
        reservationId: 'res-homeAlone-1',
        newSeatId: '21',
    },
];

const result = events.reduce<Partial<Reservation>>((currentState, event) => {
    switch (event.eventType) {
        case 'SeatReserved':
            return {
                ...currentState,
                reservationId: event.reservationId,
                movieId: event.movieId,
                seatId: event.seatId,
                userId: event.userId,
            };
        case 'SeatChanged': {
            return {
                ...currentState,
                seatId: event.newSeatId,
            };
        }
        default:
            throw 'Unexpected event type';
    }
}, {});

归功于强类型(ReservationEvents),我们可以确定事件数组的内容。我们知道这两个事件都将具有eventType属性。有了这一点,我们可以使用switch并为每个事件定义一个自定义状态变化逻辑。
剩下的唯一事情就是确保我们的最终结果具有正确的状态,并可用作Reservation 类型。请记住,reduce的结果将是Partial所有必填字段均设为可选。我们可以使用类型保护来验证是否部分也是有效的。

const reservationIsValid = 
    (reservation: Partial<Reservation>): reservation is Reservation => (
        !!reservation.reservationId &&
        !!reservation.movieId &&
        !!reservation.seatId &&
        !!reservation.userId 
    );

if(!reservationIsValid(reservation))
    throw "Reservation state is not valid!";

const reservation: Reservation = result;

 
另外,让我为您展示EventStoreDB NodeJS gRPC客户端的完整工作示例:

import { EventStoreDBClient, JSONEventType, jsonEvent } from "@eventstore/db-client";

// define types
type SeatReserved = JSONEventType<
   
"SeatReserved",
    {
        reservationId: string;
        movieId: string;
        userId: string;
        seatId: string;
    }
>;

type SeatChanged = JSONEventType<
   
"SeatChanged",
    {
        reservationId: string;
        newSeatId: string;
    }
>;

type ReservationEvents = SeatReservedEvent | SeatChangedEvent;

interface Reservation {
    reservationId: string;
    movieId: string;
    userId: string;
    seatId: string;
}

const reservationIsValid = 
    (reservation: Partial<Reservation>): reservation is Reservation => (
        !!reservation.reservationId &&
        !!reservation.movieId &&
        !!reservation.seatId &&
        !!reservation.userId 
    );

// create events
const reservationId =
"res-homeAlone-1";

const seatReserved = jsonEvent<SeatReserved>({
    type:
"SeatReserved",
    data: {
        reservationId,
        movieId:
"homeAlone",
        userId:
"ms_smith",
        seatId:
"44",
    },
});

const seatChanged= jsonEvent<SeatChanged>({
    type:
"SeatChanged",
    data: {
        reservationId,
        newSeatId: '21',
    },
});   

// connect to EventStoreDB
const client = EventStoreDBClient.connectionString(
"esdb://localhost:2113?tls=false");

// append events
const appendResult = await client.appendToStream(
    reservationId, seatReserved, seatChanged);

// readAppendedEvents
const events = await client.readStream<ReservationEvents>(reservationId);  

// aggregate stream
const result = events.reduce<Partial<Reservation>>((acc, { event }) => {
    switch (event?.type) {
        case
"SeatReserved":
            return {
                ...acc,
                reservationId: event.data.reservationId,
                movieId: event.data.movieId,
                seatId: event.data.seatId,
                userId: event.data.userId,
            };
        case
"SeatChanged": {
            return {
                ...acc,
                seatId: event.data.newSeatId,
            };
        }
        default:
            return acc;
    }
}, {});

if(!reservationIsValid(result))
    throw
"Reservation state is not valid!";

const reservation: Reservation = result;