Kodokon kodokon.com

รูปแบบสำหรับ production: branded type, satisfies และ .d.ts

นำรูปแบบระดับผู้เชี่ยวชาญมาใช้เพื่อรักษาความปลอดภัยของ codebase จริง: การจำลอง nominality, การตรวจสอบโดยไม่ขยาย type และ ambient declaration

11 นาที · 3 คำถาม

เปิดบทเรียนนี้ใน Kodokon

การกำหนด type ของ TypeScript เป็นแบบ structural: type สองตัวที่มีรูปทรงเดียวกันสามารถใช้แทนกันได้ ดังนั้น string ที่เป็น user-id จึงแยกไม่ออกจาก string ตัวอื่น ๆ Branding จำลองการกำหนด type แบบ nominal โดยทำ intersect base type กับ phantom property ที่ key เป็น unique symbol: ไม่มีค่าจริงใดมีมันเลย แต่ตอนนี้คอมไพเลอร์แยกแต่ละ brand ออกจากกันได้

TYPESCRIPT
declare const brand: unique symbol;

type Brand<T, Name extends string> =
  T & { readonly [brand]: Name };

type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;

function asUserId(raw: string): UserId {
  return raw as UserId;
}

declare function loadUser(id: UserId): void;

loadUser(asUserId("u_42")); // OK
// loadUser("u_42"); // error: bare string rejected
การจำลอง nominality โดยไม่มีต้นทุน ณ เวลารัน

โอเปอเรเตอร์ satisfies (TypeScript 4.9) เติมเต็มช่องว่างที่แม่นยำ annotation : T ขยาย type ของตัวแปรให้เป็น T และสูญเสีย literal ที่อนุมานได้; as T ปิดการตรวจสอบบางส่วน satisfies T ตรวจสอบว่า expression สอดคล้องกับ T โดยไม่แตะต้อง type ที่อนุมานได้ ซึ่งยังคงแม่นยำที่สุดเท่าที่จะเป็นไปได้ มันเป็นเครื่องมือที่เหมาะสมสำหรับ object ที่เป็นการตั้งค่า: ตรวจสอบครบถ้วน ความแม่นยำยังคงอยู่

TYPESCRIPT
type RouteDef = { path: string; auth?: boolean };

const routes = {
  home: { path: "/" },
  admin: { path: "/admin", auth: true },
} satisfies Record<string, RouteDef>;

routes.admin.auth;
// literal type true: the inferred precision is
// preserved, and an unknown key would be rejected
satisfies: การตรวจสอบที่เข้มงวดโดยไม่ขยาย type

ไฟล์ .d.ts มีเฉพาะ ambient declaration เท่านั้น: มันอธิบายค่าที่จะมีอยู่ ณ เวลารัน โดยไม่ปล่อย JavaScript ออกมาแม้แต่บรรทัดเดียว การขยาย global scope จากภายใน module นั้นบล็อก declare global เป็นสิ่งจำเป็น - และสเปกอนุญาตให้ทำได้เฉพาะในไฟล์ที่เป็น module จริง ๆ เท่านั้น จึงเป็นที่มาของสำนวน export {} ที่ด้านบนของไฟล์

TYPESCRIPT
export {};

declare global {
  interface Window {
    analytics: { track(event: string): void };
  }
}
env.d.ts: export {} เปลี่ยนไฟล์ให้เป็น module

สำหรับ dependency ของ JavaScript ที่ไม่มี type เผยแพร่มา declare module สร้าง ambient module: คอมไพเลอร์จะใช้ declaration ของคุณสำหรับทุกการ import ของแพ็กเกจนั้น ระวังขอบเขตของ skipLibCheck: เมื่อเปิดใช้งาน มันจะเพิกเฉยต่อ error จากไฟล์ .d.ts ทั้งหมด รวมถึงของคุณด้วย; ดังนั้นจงตรวจสอบ declaration ของคุณผ่าน type test ที่วางไว้ในไฟล์ .ts ทั่วไป

TYPESCRIPT
declare module "legacy-lib" {
  export interface InitOptions {
    debug?: boolean;
  }
  export function init(options?: InitOptions): void;
}
legacy-lib.d.ts: การกำหนด type ให้แพ็กเกจที่ไม่มี type เผยแพร่

ทดสอบความรู้

ตรวจสอบว่าคุณจำประเด็นสำคัญของบทเรียนนี้ได้ครบถ้วน

  1. ความแตกต่างระหว่าง satisfies T กับ annotation : T คืออะไร?
    • satisfies ขยาย type ของ expression ให้เป็น T
    • satisfies ตรวจสอบความสอดคล้องกับ T ในขณะที่ยังคง type ที่อนุมานได้ซึ่งแม่นยำกว่าไว้
    • satisfies ปิดการตรวจสอบ เหมือน as
    • ไม่มีความแตกต่าง: ทั้งสองรูปแบบสมมูลกัน
  2. ณ เวลารัน ค่าที่ผ่านการ brand ซึ่งสร้างด้วย Brand<string, ...> มีอะไรอยู่ข้างใน?
    • สตริงพร้อมกับ property ที่เป็น symbol
    • สตริงธรรมดา: brand มีอยู่เฉพาะ ณ เวลาคอมไพล์เท่านั้น
    • object ที่ห่อสตริงต้นฉบับไว้
  3. ทำไมจึงเพิ่ม export {} ที่ด้านบนของไฟล์ .d.ts ที่ใช้ declare global?
    • เพื่อ export global type ไปยังไฟล์อื่น
    • เพื่อเปลี่ยนไฟล์ให้เป็น module ซึ่งเป็นเงื่อนไขที่ declare global ต้องการ
    • เพื่อเปิดใช้งาน strict mode โดยอัตโนมัติ
    • มันเป็นเพียงการตกแต่งเท่านั้น