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-pattern
via yarn
yarn add ts-pattern
via 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
จบไปแล้วนะครับสำหรับบทความนี้ เจอกันใหม่บทความหน้าครับ