เก็บ Type Guards ไว้หลังจากทำการ refactoring ใน TypeScript

Dec 04 2022
ตัวป้องกันประเภททั่วไปช่วยให้โค้ดมีข้อบกพร่องน้อยลง
TypeScript เป็นภาษาที่ยอดเยี่ยมสำหรับการเขียนแอปพลิเคชันที่มีการตรวจสอบความปลอดภัยของประเภท การขยายหรือปรับโครงสร้างโค้ดใน TypeScript ทำได้ง่ายกว่า JavaScript ธรรมดามาก

TypeScript เป็นภาษาที่ยอดเยี่ยมสำหรับการเขียนแอปพลิเคชันที่มีการตรวจสอบความปลอดภัยของประเภท การขยายหรือปรับโครงสร้างโค้ดใน TypeScript ทำได้ง่ายกว่า JavaScript ธรรมดามาก

TypeScript มีฟังก์ชันในตัวที่ดีสำหรับการจำกัดอินเทอร์เฟซ — Type Guards แต่ก็ไม่ได้ป้องกันข้อผิดพลาดในระหว่างการขยายรหัส / การปรับโครงสร้างรหัสเสมอไป โดยเฉพาะอย่างยิ่งหากคุณมีโครงการขนาดใหญ่

คำอธิบายปัญหา

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

import React from "react";

type Tea = { id: number; name: string; price: number };

type LooseTea = Tea & { weight: number };
type BaggedTea = Tea & { size: number };

const isLooseTea = (x: Tea): x is LooseTea => "weight" in x;
const isBaggedTea = (x: Tea): x is BaggedTea => "size" in x;

const getLink = (x: Tea): string => {
  if (isLooseTea(x)) return `/loose-tea/${x.id}`;
  if (isBaggedTea(x)) return `/bagged-tea/${x.id}`;
  throw new Error("Unknown tea");
};

const getTitle = (x: Tea): string => {
  if (isLooseTea(x)) return `${x.name} / ${x.weight}g`;
  if (isBaggedTea(x)) return `${x.name} / ${x.size} teabags`;
  throw new Error("Unknown tea");
};

const TeaItems: React.FC<{ items: Tea[] }> = ({ items }) => {
  return (
    <ul>
      {items.map((x) => (
        <li key={x.id}>
          <a href={getLink(x)}>{getTitle(x)}</a>
        </li>
      ))}
    </ul>
  );
};

type BaggedTea = Tea & { bags: number; weightPerBag: number };

// before:
const getTitle = (x: Tea): string => {
  if (isLooseTea(x)) return `${x.name} / ${x.weight}g`;
  // Property 'size' does not exist on type 'BaggedTea'. ts(2339)
  if (isBaggedTea(x)) return `${x.name} / ${x.size} teabags`;
  throw new Error("Unknown tea");
};

// after:
const getTitle = (x: Tea): string => {
  if (isLooseTea(x)) return `${x.name} / ${x.weight}g`;
  if (isBaggedTea(x)) return `${x.name} / ${x.bags} teabags ~ ${x.weightPerBag}`;
  throw new Error("Unknown tea");
};

วิธีแก้ปัญหา

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

มาจัดการกันเถอะ ฉันเสนอให้ใช้ฟังก์ชันโรงงานเพื่อสร้างฟังก์ชัน Type Guard ตามฟิลด์แบบจำลองข้อมูล

type AnyObject = Record<string, any>;

// https://stackoverflow.com/a/52991061
type RequiredKeys<T> = {
  [K in keyof T]-?: AnyObject extends Pick<T, K> ? never : K
}[keyof T];

const createShapeGuard = <T extends AnyObject>(...keys: RequiredKeys<T>[]) => {
  return (obj: unknown): obj is T => {
    if (typeof obj !== "object" || obj === null) return false;

    for (const key of (keys as string[])) {
      if (!(key in obj)) return false;
    }

    return true;
  };
};

const isLooseTea = createShapeGuard<LooseTea>("weight")
const isBaggedTea = createShapeGuard<BaggedTea>("bags", "weightPerBag")

type BaggedTea = Tea & { bags: number; bagWeight: number };

// Argument of type '"weightPerBag"' is not assignable to parameter of type 'RequiredKeys<BaggedTea>'.ts(2345)
const isBaggedTea = createShapeGuard<BaggedTea>("bags", "weightPerBag")