คุณจะจำลองสถานะต่าง ๆ ด้วย discriminated union และล็อกความครบถ้วนของมันไว้ด้วยชนิด never
เปิดบทเรียนนี้ใน Kodokonnever คือชนิดว่างเปล่า: ไม่มีค่าใดอาศัยอยู่ในนั้น มันปรากฏขึ้นเมื่อคอมไพเลอร์พิสูจน์ได้ว่ามีเส้นทางโค้ดที่เป็นไปไม่ได้ - เช่นหลังจากไล่จนครบทุกสมาชิกของ union แล้ว คุณสมบัตินี้ทำให้มันเป็นเครื่องมือออกแบบ: ถ้าคุณทำให้คอมไพเลอร์บอกว่า never ในสาขา default ของ switch ได้ คุณก็มีหลักฐานแบบ static ว่าทุกกรณีถูกจัดการแล้ว นี่คือรากฐานของ การตรวจความครบถ้วน (exhaustiveness checking)
คุณได้รู้จัก discriminated union มาแล้วในโมดูลก่อนหน้า ตอนนี้ถึงเวลาเปลี่ยนมันให้เป็นเครื่องมือออกแบบตัวจริง เมื่อเทียบกับ interface เดียวที่ยัดฟิลด์ optional เข้าไปมากมาย discriminated union ทำให้ สถานะที่เป็นไปไม่ได้แสดงตัวออกมาไม่ได้: คุณไม่สามารถมีทั้ง data และ error พร้อมกัน หรือมีความสำเร็จโดยไม่มีข้อมูล มันคือรูปแบบที่นิยมใช้กับสถานะการโหลด ผลของการดำเนินการ และข้อความเหตุการณ์
type FetchState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };
function render(state: FetchState<string[]>): string {
switch (state.status) {
case "idle": return "Waiting";
case "loading": return "Loading…";
case "success": return state.data.join(", ");
case "error": return state.error.message;
}
}คุณเคยพบ guard assertNever ที่ถูกเรียกในสาขา default มาแล้ว ตอนนี้มาผ่ามันดูว่ามันทำงานอย่างไรกันแน่ ตราบใดที่ทุกกรณีถูกครอบคลุม พารามิเตอร์ของมันจะมีชนิด never ตรงนั้นและทุกอย่างคอมไพล์ผ่าน แต่ถ้าเพิ่มตัวแปรเข้าไปใน union โดยไม่อัปเดต switch ตัวแปรที่ถูกลืมจะไม่สามารถ assign ให้ never ได้อีกต่อไป: error ตอนคอมไพล์ พร้อมชื่อของกรณีที่หายไปในข้อความ คุณเปลี่ยนบั๊กตอน production ให้กลายเป็น build ที่ถูกปฏิเสธ
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "rect"; width: number; height: number };
function assertNever(value: never): never {
throw new Error("Unhandled case: " + String(value));
}
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "square": return shape.side ** 2;
case "rect": return shape.width * shape.height;
default: return assertNever(shape);
}
}มีความละเอียดอ่อนขั้นสูงสองข้อที่ควรใส่ใจ ข้อแรก การ narrow ด้วยตัวแบ่งแยกจะแพร่ไปยังโครงสร้างที่ซ้อนกันก็ต่อเมื่อคุณตรวจอ็อบเจกต์ที่ห่อหุ้มโดยตรง การ destructure ตัวแบ่งแยกออกมาเดี่ยว ๆ จะตัดความเชื่อมโยงนั้น (ยกเว้นกรณีที่รองรับตั้งแต่ TypeScript 4.6 สำหรับการ destructure ร่วมกัน) ข้อสอง never ถูกดูดกลืนใน union (string | never ก็คือ string): นี่คือสิ่งที่ทำให้ conditional type กรองตัวแปรออกได้ เหมือนที่ Exclude ทำ
type Event =
| { type: "click"; x: number; y: number }
| { type: "keypress"; key: string };
type EventOf<T extends Event["type"]> =
Extract<Event, { type: T }>;
type ClickEvent = EventOf<"click">;
function handle<T extends Event["type"]>(
type: T,
handler: (event: EventOf<T>) => void
): void {
/* register the handler */
}
handle("click", (e) => console.log(e.x, e.y));Shape โดยไม่แก้ switch ที่มี assertNever?string | never คืออะไร?neverstringunknownstring | undefined