วิธีการจัดโครงสร้างรูปแบบเหตุการณ์ของ 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 ไปยังโค้ด
