Kodokon kodokon.com

Template literal type: สตริงที่คำนวณ ณ เวลาคอมไพล์

ใช้ประโยชน์จาก template literal type เพื่อสร้าง union ผ่าน Cartesian product และแยกวิเคราะห์ string literal ด้วย infer

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

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

Template literal type นำไวยากรณ์ template literal เข้าสู่โลกของ type การแทรก (interpolate) union เข้าไป คอมไพเลอร์จะคำนวณ Cartesian product: ทุกการผสมที่เป็นไปได้กลายเป็นสมาชิกหนึ่งของผลลัพธ์ นี่คือเครื่องมือที่ให้คุณกำหนด type ให้กับ event key, route หรือ token ของ design system ได้ลงลึกถึงตัวอักษรที่แน่นอน

TYPESCRIPT
type Lang = "fr" | "en";
type Theme = "light" | "dark";

type ThemeKey = `${Lang}-${Theme}`;
// "fr-light" | "fr-dark" | "en-light" | "en-dark"
union สองตัวที่ถูกแทรก: Cartesian product ที่มี 4 สมาชิก

พลังที่แท้จริงปรากฏออกมาเมื่อรวมกับ infer: template literal type กลายเป็น รูปแบบการจับคู่ (matching pattern) บนสตริง คอมไพเลอร์ตัดแบ่ง input literal เพื่อจับคู่แต่ละส่วน; เมื่อการแบ่งเป็นไปได้หลายทาง การปรากฏครั้งแรก ของตัวคั่นจะชนะ เพิ่ม recursion เข้าไปแล้วคุณจะได้ตัวแยกวิเคราะห์ (parser) ที่แท้จริงซึ่งประเมิน ณ เวลาคอมไพล์

TYPESCRIPT
type Split<S extends string, Sep extends string> =
  S extends `${infer Head}${Sep}${infer Rest}`
    ? [Head, ...Split<Rest, Sep>]
    : [S];

type Parts = Split<"a.b.c", ".">;
// ["a", "b", "c"]
การแยก (split) แบบ recursive ที่เป็น static ทั้งหมด

รูปแบบนี้เปล่งประกายเมื่อสกัดพารามิเตอร์ของ route สไตล์ Express การเรียก recursive แต่ละครั้งกิน :param หนึ่งส่วน และ union สุดท้ายจะแสดงรายชื่อพารามิเตอร์ทุกตัว ซึ่งคุณสามารถป้อนเข้าไปใน Record เพื่อกำหนด type ให้กับ signature ของ handler ได้

TYPESCRIPT
type Params<Path extends string> =
  Path extends `${string}:${infer P}/${infer Rest}`
    ? P | Params<Rest>
    : Path extends `${string}:${infer P}`
      ? P
      : never;

type RouteParams = Params<"/users/:id/posts/:postId">;
// "id" | "postId"
การสกัดพารามิเตอร์ของ route ณ เวลาคอมไพล์

TypeScript มาพร้อมยูทิลิตี intrinsic สี่ตัว - Uppercase, Lowercase, Capitalize, Uncapitalize - ที่ implement โดยตรงภายในคอมไพเลอร์ (คีย์เวิร์ด intrinsic) จึงเขียนใหม่เองไม่ได้ อีกหนึ่งรูปแบบสำหรับ production: string & {} ใน union นั้น string จะดูดกลืน literal และ editor จะสูญเสียการเติมข้อความอัตโนมัติ; การทำ intersect กับ {} สร้าง type ที่สมมูลแต่แตกต่างซึ่งคอมไพเลอร์ไม่ลดทอน

TYPESCRIPT
type Method = "get" | "post";
type Handler = `on${Capitalize<Method>}`;
// "onGet" | "onPost"

type KnownColor = "red" | "green" | "blue";
type Color = KnownColor | (string & {});

declare function paint(color: Color): void;

paint("red");     // suggested by autocompletion
paint("#ff8800"); // also accepted
intrinsic และเทคนิค string & {} เพื่อการเติมข้อความอัตโนมัติ

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

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

  1. หาก A มี 3 สมาชิก และ B มี 4 สมาชิก แล้ว ${A}-${B} มีกี่สมาชิก?
    • 7
    • 12
    • 1
    • 4
  2. Capitalize ผลิตอะไรเมื่อนำไปใช้กับ literal "hello world"?
    • "Hello World"
    • "Hello world"
    • "HELLO WORLD"
  3. ทำไมจึงเขียน KnownColor | (string & {}) แทนที่จะเป็น KnownColor | string?
    • เพื่อห้ามสตริงที่อยู่นอก KnownColor
    • เพื่อป้องกันไม่ให้ string ดูดกลืน literal และรักษาการเติมข้อความอัตโนมัติไว้
    • เพื่อปรับปรุงประสิทธิภาพของคอมไพเลอร์
    • ทั้งสองรูปแบบสมมูลกันอย่างเคร่งครัด