ts-pattern เครื่องมือที่จะมาช่วยให้ทำ Pattern matching ใน TypeScript
14 Jul 2023 18:00
Written by: Yosapol Jitrak
บทความที่แล้ว ผมได้กล่าวถึง Switch true และได้เกริ่นไว้หน่อยแล้วว่า จะมี Tool ที่จะมาช่วยทำให้ Code มันสะอาดขึ้น สิ่งนั้นคือ ts-pattern
มันทำอะไรได้บ้าง ก่อนอื่นเลย เขาเขียนจั่วหัวไว้ดังนี้ “The exhaustive Pattern Matching library for TypeScript with smart type inference.” ดูเท่ และน่าสนใจมากเลย
เรื่องติดตั้งก็ไม่มีอะไรมากครับ ลงทีเดียวจบ ไม่ต้อง Set อะไรเพิ่มแล้ว
via npm
npm install ts-patternvia yarn
yarn add ts-patternvia pnpm
pnpm add ts-patternผมลองยกตัวอย่างโจทย์เรื่องชนะทางแพ้ทางของเกม จากบทความที่แล้วนะครับ
graph LR
    water --> |2| fire
    fire --> |2| grass
    grass --> |2| earth
    earth --> |2| electric
    electric --> |2| water
    dark --> |1| light
    dark --> |1.2| water
    dark --> |1.2| fire
    dark --> |1.2| grass
    dark --> |1.2| earth
    dark --> |1.2| electric
    light --> |3| dark
    light --> |0.9| water
    light --> |0.9| fire
    light --> |0.9| grass
    light --> |0.9| earth
    light --> |0.9| electricยกตัวอย่างข้อมูลแถวแรก คือ น้ำชนะไฟ Damage จะคูณ 2
ถ้าเขียนใน Version ที่ใช้ ts-pattern จะเป็นดังนี้ครับ
import { match, P } from 'ts-pattern';
export const allElements = ['normal', 'water', 'fire', 'grass', 'earth', 'electric', 'light', 'dark'] as const;
export type ElementType = (typeof allElements)[number];
export type CalculateMultiplierInput = [ElementType, ElementType];
// water -> fire -> grass -> earth -> electric -> water | 2
// dark -> every | 1.2
// light -> dark | 3
// light -> not dark | 0.9
export const calculateDamageMultiplier = (input: CalculateMultiplierInput) =>
  match(input)
    .with(['water', 'fire'], () => 2)
    .with(['fire', 'grass'], () => 2)
    .with(['grass', 'earth'], () => 2)
    .with(['earth', 'electric'], () => 2)
    .with(['electric', 'water'], () => 2)
    .with(['dark', P.string], () => 1.2)
    .with(['light', 'dark'], () => 3)
    .with(['light', P.string], () => 0.9)
    .otherwise(() => 1);Code ดูเป็นมิตรกับเรามากขึ้นกว่า Version switch true มากขึ้นเยอะเลยครับ
ท่านี้คือการใช้ Tuple pattern ครับ โดย P.string จะหมายถึง Wildcards อะไรก็ได้ที่เป็น string
P ย่อมาจาก Pattern นะครับ สามารถใช้แทนกันได้
with จะมีโครงสร้างดังนี้ครับ .with(pattern, handler)
ลองมาดูตัวอย่างอื่นกันครับ
import { match, P } from 'ts-pattern';
type Input = [number, '+', number] | [number, '-', number] | [number, '*', number] | ['-', number];
const input: Input = [3, '*', 4];
const output = match(input)
  .with([P.number, '+', P.number], ([x, , y]) => x + y)
  .with([P.number, '-', P.number], ([x, , y]) => x - y)
  .with([P.number, '*', P.number], ([x, , y]) => x * y)
  .with(['-', P.number], ([, x]) => -x)
  .otherwise(() => NaN);Credit: https://github.com/gvergnaud/ts-pattern
เคสนี้คือใช้ P.numbers หมายถึง Wildcards ที่เป็น number อะไรก็ได้ครับ
คราวนี้ลองยกตัวอย่างเป็นโปรแกรมตัดเกรดนักศึกษา Version ts-pattern กันครับ
import { match } from 'ts-pattern';
enum Grade {
  A = 'A',
  B = 'B',
  C = 'C',
  D = 'D',
  F = 'F',
}
interface Student {
  id: number;
  name: string;
  score: number;
  grade?: string;
}
const students: Student[] = [
  { id: 1, name: 'Alice', score: 85 },
  { id: 2, name: 'Bob', score: 72 },
  { id: 3, name: 'Charlie', score: 56 },
  { id: 4, name: 'David', score: 65 },
  { id: 5, name: 'Eve', score: 49 },
];
students.forEach((student) => {
  student.grade = match(student.score)
    .when(
      (score) => score >= 80,
      () => Grade.A,
    )
    .when(
      (score) => score >= 70,
      () => Grade.B,
    )
    .when(
      (score) => score >= 60,
      () => Grade.C,
    )
    .when(
      (score) => score >= 50,
      () => Grade.D,
    )
    .otherwise(() => Grade.F);
  console.table(students);
});โจทย์นี้เราจะใช้ .when แทน if-else หรือ switch case นะครับ
โดย when จะมีโครงสร้างดังนี้ครับ .when(predicate, handler)
บางคนอาจจะไม่รู้จัก Predicate นะครับ Predicate คือ Function ที่คืนค่าเป็น Boolean ครับ
ต่อไปจะกล่าวถึง Feature ที่ผมชอบที่สุดของ ts-pattern นะครับ
แต่ก่อนจะไปตรงนั้น เรามาดูปัญหาก่อน สมมติว่าเราเขียน switch case ปกติ โดยใช้ Enum type นะครับ
enum ProxyType {
  AccountNumber = 'ACCNO',
  MSISDN = 'MSISDN',
  NationId = 'NATID',
}
Object.values(ProxyType).forEach((proxyType) => {
  let message: string;
  switch (proxyType) {
    case ProxyType.AccountNumber:
      message = 'AccountNumber';
      break;
    case ProxyType.MSISDN:
      message = 'MSISDN';
      break;
    case ProxyType.NationId:
      message = 'NationId';
      break;
  }
  console.log(message);
});ถ้าวันดีคืนดี เกิดมี Type ประเภทใหม่ขึ้นมา แล้วมี Switch case เหล่านี้เต็มโปรเจคไปหมด เรามีโอกาสผิดพลาดได้ครับ เพราะ Code switch case เหล่านี้ ก็ Complie ผ่าน อาจจะไปเกิด Bug บน Production ได้ โดยมีคนเขียนบทความถึงเรื่องนี้อยู่ครับ คิดซักนิดก่อนใช้ switch
Feature ของ ts-pattern ที่ว่าคือ exhaustive จะช่วยเตือนเรา มีแจ้งเตือนเราตั้งแต่ใน IDE ครับ และ Compile ไม่ผ่านตั้งแต่แรก

อันนี้ผมลืมบอกไป ตอนสุดท้ายหลังจากใส่ Pattern แล้ว เราจะต้องสั่ง Run มัน โดยจะมี Run อยู่ 3 ประเภท คือ
โดยส่วนตัวก็แนะนำให้ใช้แค่ exhaustive กับ otherwise ส่วนของ run เลี่ยงได้เลี่ยงครับ
จริง ๆ มี Feature และ Use case ให้ใช้ได้เยอะกว่านี้มากครับ สำหรับ ts-pattern ลองเข้าไปดูเองได้ที่ Document เขากันครับ ts-pattern
จบไปแล้วนะครับสำหรับบทความนี้ เจอกันใหม่บทความหน้าครับ