ค้นหาเว็บไซต์

วิธีการจัดโครงสร้างรูปแบบเหตุการณ์ของ TypeScript เพื่อรองรับอาร์กิวเมนต์ของมิดเดิลแวร์เฉพาะเหตุการณ์?


ฉันกำลังทำงานเกี่ยวกับกรอบการทำงานแอปพลิเคชันที่ขับเคลื่อนด้วยเหตุการณ์ซึ่งใช้รูปแบบมิดเดิลแวร์เพื่อประมวลผลเหตุการณ์ที่ถูกส่งจากบริการของฉันไปยังแอปพลิเคชันของบุคคลที่สาม บริการของฉันสามารถสร้างแบบจำลองเหตุการณ์หลายประเภทที่แอปพลิเคชันของบุคคลที่สามอาจต้องการลงทะเบียนและรับ

เมื่อ payload ของเหตุการณ์ถูกรับโดยเฟรมเวิร์ก มันจะถูก 'เสริม' ด้วยส่วนเพิ่มเติมเฉพาะของ middleware เช่น ฟังก์ชัน next() และ context ที่ผู้ใช้กำหนดได้ - เป็นเรื่องมาตรฐานของ middleware อย่างไรก็ตาม เหตุการณ์บางอย่างมีการเสริมเฉพาะเพิ่มเติมที่ใช้ได้เฉพาะกับเหตุการณ์บางประเภทเท่านั้น ตัวอย่างเช่น เหตุการณ์ข้อความจะมี property message เหตุการณ์ foo อาจมี property foo เป็นต้น - แม้ว่าประเภท/ชื่อของเหตุการณ์และการเสริมเฉพาะของเหตุการณ์อาจไม่ตรงกันแบบหนึ่งต่อหนึ่งโดยสมบูรณ์และ/หรืออาจมีการเสริมหลายอย่าง

วิธีที่การปรับแต่งเฉพาะกิจกรรมเหล่านี้ถูกกำหนดประเภทในเฟรมเวิร์กแอปในปัจจุบันมีปัญหาและมักทำให้เกิดข้อผิดพลาด; ผลลัพธ์คือ โค้ดเบสใช้การยืนยันประเภท (as) เป็นจำนวนมาก - น่าจะเป็นสัญญาณที่ไม่ดี ต้องยอมรับว่าผมไม่ใช่ผู้เชี่ยวชาญ TypeScript และโค้ดของเฟรมเวิร์กมีมาก่อนที่ผมจะเข้ามาเกี่ยวข้อง; ผมแค่คิดว่า 'ก็ประมาณนี้แหละที่โปรเจกต์ TS จะถูกสร้างขึ้น' หลังจากผ่านไปสองสามปีและเรียนรู้เกี่ยวกับ TypeScript มากขึ้น ผมคิดว่าตอนนี้ต้องมีวิธีที่ดีกว่านี้แน่ ๆ!

นี่คือสนามเด็กเล่น TypeScript สำหรับตัวอย่าง ซึ่งฉันก็ได้คัดลอกไว้ด้านล่างด้วย เมธอด wrapMiddleware แสดงข้อผิดพลาดดังนี้:

Argument of type 'MiddlewareArgs<"message">' is not assignable to parameter of type 'MiddlewareArgs<string>'.
  Types of property 'message' are incompatible.
    Type 'MsgEvent' is not assignable to type 'undefined'.
import { expectAssignable } from 'tsd';

// A couple of events, and a union of all events (there are more in actuality)
interface MsgEvent {
  type: 'message';
  text: string;
  channel: string;
  user: string;
}
interface JoinEvent {
  type: 'join';
  channel: string;
  user: string;
}
type AllEvents = MsgEvent | JoinEvent;

// Utility types to 'extract' event payloads out based on the `type` property
type KnownEventFromType<T extends string> = Extract<AllEvents, { type: T }>;
type EventFromType<T extends string> = KnownEventFromType<T> extends never ? { type: T } : KnownEventFromType<T>;

// A contrived example of arguments passed to middleware
interface MiddlewareArgs<EventType extends string = string> {
  event: EventFromType<EventType>;
  message: EventType extends 'message' ? this['event'] : undefined; // <-- problematic; there must be a better way, right?
}

// Augmenting events with additional middleware things
type AllMiddlewareArgs = {
  next: () => Promise<void>;
}
function wrapMiddleware<Args extends MiddlewareArgs>(
  args: Args,
): Args & AllMiddlewareArgs {
  return {
    ...args,
    next: async () => {},
  }
}

// And now for the actual example:
const messageEvt: MsgEvent = {
  type: 'message',
  channel: 'random',
  user: 'me',
  text: 'hello world',
}
const messageEvtArgs: MiddlewareArgs<'message'> = {
  event: messageEvt,
  message: messageEvt,
}
const joinEvt: JoinEvent = {
  type: 'join',
  channel: 'random',
  user: 'me'
}
const joinEvtArgs: MiddlewareArgs<'join'> = {
  event: joinEvt,
  message: undefined, // <-- bonus points if we can get rid of having to set an undefined message!
}

// Some test cases
expectAssignable<AllMiddlewareArgs>(wrapMiddleware(messageEvtArgs));
expectAssignable<MiddlewareArgs<'message'>>(wrapMiddleware(messageEvtArgs));
expectAssignable<AllMiddlewareArgs>(wrapMiddleware(joinEvtArgs));
expectAssignable<MiddlewareArgs<'join'>>(wrapMiddleware(joinEvtArgs));
// Wrapping random untyped events should fallback
expectAssignable<AllMiddlewareArgs>(wrapMiddleware({ event: { type: 'garbage' }}));
expectAssignable<MiddlewareArgs<'garbage'>>(wrapMiddleware({ event: { type: 'garbage' }}));

ฉันเข้าใจ ว่าทำไม ฉันถึงได้ข้อผิดพลาดที่เกิดขึ้น: เมธอด wrapMiddleware ยอมรับชนิด MiddlewareArgs ที่กว้างกว่าและไม่เฉพาะเจาะจงกับ message ซึ่งตั้งค่าพารามิเตอร์ทั่วไปให้เป็น string ดังนั้นคุณสมบัติ message ของอินเทอร์เฟซอาจเป็นทั้งวัตถุข้อความหรือ undefined ตามชนิดเงื่อนไขของคุณสมบัติ message คำถามของฉันคือ: วิธีที่ดีกว่าในการจัดโครงสร้างแนวทางนี้เพื่อให้สามารถขยายไปยังอีเวนต์อื่น ๆ ได้โดยมีรูปแบบอาร์กิวเมนต์มิดเดิลแวร์เฉพาะอีเวนต์ที่ต่างกันคืออะไร?

คำตอบ #1

เพื่อให้การทดสอบทั้งหมดของคุณผ่าน วิธีที่ง่ายที่สุดโดยมากคือการอนุญาตให้ wrapMiddleware() รับอินพุตเป็นวัตถุ ใดก็ได้:

function wrapMiddleware<A extends object>(a: A): A & AllMiddlewareArgs {
  return {
    ...a,
    next: async () => { },
  }
}

// all okay
expectAssignable<AllMiddlewareArgs>(wrapMiddleware(messageEvtArgs));
expectAssignable<MiddlewareArgs<'message'>>(wrapMiddleware(messageEvtArgs));
expectAssignable<AllMiddlewareArgs>(wrapMiddleware(joinEvtArgs));
expectAssignable<MiddlewareArgs<'join'>>(wrapMiddleware(joinEvtArgs));
expectAssignable<AllMiddlewareArgs>(wrapMiddleware({ event: { type: 'garbage' } }));
expectAssignable<MiddlewareArgs<'garbage'>>(wrapMiddleware({ event: { type: 'garbage' } }));

นั่นก็เพราะว่า ในระดับพื้นฐาน สิ่งที่ wrapMiddleware ทำจริง ๆ คือกระจายข้อมูลนำเข้าไปยังอ็อบเจกต์ใหม่พร้อมกับเพิ่มเมธอด next ของประเภทเขียนเข้าไป ถ้าคุณแค่สนใจให้การทดสอบเหล่านี้ผ่าน นี่คือวิธีที่คุณควรทำ มันง่ายมาก


เหตุผลเดียวที่คุณจะทำสิ่งอื่นคือถ้าคุณต้องการที่จะ ปฏิเสธ ข้อมูลนำเข้าที่ไม่ตรงกับข้อจำกัดบางอย่างที่อยู่นอกกรอบ:

function wrapMiddleware<A extends { event: { type: string } }>(
  a: A & MiddlewareArgs<A["event"]["type"]>
): A & AllMiddlewareArgs {
  return {
    ...a,
    next: async () => { },
  }
}

ตอนนี้เราจะยอมรับเฉพาะ a ที่มีชนิด A เป็นสิ่งที่มีคุณสมบัติ type ที่มีค่าเป็น string ในคุณสมบัติ event ของมัน และยิ่งไปกว่านั้น จะยอมรับเฉพาะสิ่งที่สามารถกำหนดค่าให้กับ MiddlewareArgs<A["event"]["type"]> ได้เท่านั้น นี่เป็นการตรวจสอบเพื่อให้แน่ใจว่าชนิดที่ทราบมีคุณสมบัติที่คุณสนใจ ส่วนใหญ่คุณสามารถปล่อยคำจำกัดความของคุณไปตามเดิมได้ แต่ส่วนสำคัญที่อนุญาตให้ message หายไปและไม่เป็น undefined เว้นแต่ type จะเป็น "message" คือการเปลี่ยน MiddlewareArgs ของคุณไปเป็นเวอร์ชันที่วางชนิดตามเงื่อนไขในระดับสูงขึ้น เพื่อให้ { message: MsgEvent } เป็นสิ่งที่จำเป็นหรือไม่ก็ได้:

type MiddlewareArgs<K extends string = string> = {
  event: EventFromType<K>;
} & (K extends "message" ? { message: MsgEvent } : unknown)

โปรดทราบว่าประเภท unknown เป็นองค์ประกอบเอกลักษณ์สำหรับการตัดกัน ดังนั้น {event: EventFromType<K>} & unknown จึงเป็นเพียง {event: EventFromType<K>}

การทดสอบที่มีอยู่ของคุณยังผ่านอยู่ แต่ตอนนี้บางสิ่งอาจล้มเหลวได้ เช่น:

wrapMiddleware({ event: { type: "message", channel: "", text: "", user: "" } }); // error!
//             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Property 'message' is missing
wrapMiddleware({ event: { type: "join", user: "" } }); // error!
//               ~~~~~
// Property 'channel' is missing

ลิงก์ Playground ไปยังโค้ด