DEV Community

Cover image for มองข้าม TypeScript: ทำไม Type Safety ที่แท้จริงต้องไปไกลกว่าแค่ compile time
r1ACK
r1ACK

Posted on

มองข้าม TypeScript: ทำไม Type Safety ที่แท้จริงต้องไปไกลกว่าแค่ compile time

JavaScript ถือกำเนิดมาพร้อมปรัชญาความยืดหยุ่นสุดขั้ว ตัวแปรตัวหนึ่งสามารถเปลี่ยนชนิดข้อมูลได้ตลอดเวลาโดยไม่มีการเตือนจากตัวภาษาเลยแม้แต่น้อย ในยุคที่เว็บไซต์ยังเป็นเพียงหน้าเอกสารเรียบง่าย ความยืดหยุ่นนี้คือจุดแข็ง แต่เมื่อแอปพลิเคชันยุคใหม่เติบโตขึ้นเป็นระบบที่มีโค้ดหลายแสนบรรทัด ทีมงานหลายสิบคนทำงานพร้อมกัน และมีการเชื่อมต่อกับระบบภายนอกนับไม่ถ้วน ความยืดหยุ่นแบบไร้ขอบเขตกลับกลายเป็นแหล่งกำเนิดบั๊กที่ตรวจจับได้ยากที่สุดในขั้นตอนการพัฒนา

TypeScript จึงถือกำเนิดขึ้นมาเพื่อแก้ปัญหานี้ ด้วยการเติมระบบ static type checking เข้าไปในภาษา ช่วยให้นักพัฒนาจับข้อผิดพลาดเกี่ยวกับชนิดข้อมูลได้ตั้งแต่ตอนเขียนโค้ด ก่อนที่จะถูกนำไปรันจริงบนเครื่องผู้ใช้ ด้วยเหตุนี้ TypeScript จึงกลายเป็นมาตรฐานโดยพฤตินัยของวงการพัฒนาเว็บสมัยใหม่ในเวลาไม่นาน

แต่คำถามที่วิศวกรระดับซีเนียร์เริ่มตั้งคำถามกันมากขึ้นเรื่อยๆ คือ เราพึ่งพา TypeScript มากเกินไปจนลืมมองข้อจำกัดของมันไปแล้วหรือเปล่า คำตอบคือใช่ และบทความนี้จะพาไปสำรวจว่าทำไมถึงเป็นเช่นนั้น พร้อมแนะนำเครื่องมือรุ่นใหม่ที่กำลังเข้ามาเติมเต็มช่องว่างสำคัญนี้

จุดอ่อนที่ TypeScript แก้ไม่ได้ด้วยตัวเอง

หายไปตอนรันไทม์: ธรรมชาติของ Type Erasure
สิ่งสำคัญที่สุดที่ต้องทำความเข้าใจให้ถ่องแท้คือ TypeScript ทำหน้าที่เป็นเพียงชั้นตรวจสอบในขั้นตอน compile time เท่านั้น เมื่อโค้ดถูกแปลงเป็น JavaScript ล้วนๆ เพื่อนำไปรันจริง ข้อมูล type ทั้งหมดจะถูกลบทิ้งไปโดยสิ้นเชิงในกระบวนการที่เรียกว่า type erasure นั่นหมายความว่าไม่ว่าคุณจะประกาศ type ไว้รัดกุมแค่ไหนในซอร์สโค้ด เมื่อแอปพลิเคชันทำงานจริงบน production จะไม่มีการตรวจสอบชนิดข้อมูลใดๆ หลงเหลืออยู่เลยแม้แต่น้อย

ปัญหานี้เห็นได้ชัดที่สุดในจุดที่ข้อมูลไหลเข้ามาจากภายนอกระบบ เช่น การเรียก REST API แล้วใช้คำสั่ง as เพื่อบังคับให้ response ตรงกับ type ที่กำหนดไว้ หรือการอ่านข้อมูลจาก localStorage, URL query string, หรือฟอร์มที่ผู้ใช้กรอกเข้ามา ในทุกกรณีเหล่านี้ TypeScript compiler จะเชื่อคำประกาศ type ที่คุณเขียนไว้ทันทีโดยไม่มีการตรวจสอบข้อมูลจริงเลยแม้แต่น้อยว่าตรงตามโครงสร้างที่ระบุหรือไม่ หาก API เปลี่ยนโครงสร้างข้อมูลกะทันหันโดยไม่แจ้งเตือนล่วงหน้า แอปพลิเคชันจะพังทันทีตอนรันไทม์ ทั้งที่ตอน build ไม่มีสัญญาณเตือนใดๆ จาก compiler เลย

any และ unknown: ประตูหลังที่ยังเปิดอยู่เสมอ
แม้ TypeScript จะมี strict mode ที่ช่วยลดโอกาสการใช้ any โดยไม่ตั้งใจ แต่ในโปรเจกต์ขนาดใหญ่จริง โดยเฉพาะโปรเจกต์ที่ migrate มาจาก JavaScript เดิม หรือที่ต้องทำงานร่วมกับ library ภายนอกที่มี type definition ไม่สมบูรณ์ นักพัฒนามักหลีกเลี่ยงไม่ได้ที่จะใช้ any เพื่อ "ปิดปาก" compiler แล้วเดินหน้าต่อ ซึ่งเท่ากับปิดกั้นการตรวจสอบ type ในจุดนั้นโดยสมบูรณ์ ยิ่งโปรเจกต์เติบโตขึ้น จำนวนจุดที่แอบใช้ any ก็มักเพิ่มขึ้นตามไปด้วยอย่างเงียบๆ จนกลายเป็นหลุมดำที่บั่นทอนความน่าเชื่อถือของระบบ type ทั้งหมดในระยะยาว

เมื่อ Structural Typing หลวมเกินไปสำหรับความหมายทางธุรกิจ
TypeScript ใช้แนวคิด structural typing ซึ่งถือว่า type สองตัวเข้ากันได้ตราบใดที่มีโครงสร้างหน้าตาเหมือนกัน แม้ความหมายทางธุรกิจจะแตกต่างกันโดยสิ้นเชิงก็ตาม ตัวอย่างที่พบบ่อยคือ type อย่าง UserId และ ProductId ที่ทั้งคู่เป็น string เหมือนกัน สามารถสลับใช้แทนกันได้โดย compiler ไม่ทักท้วงใดๆ เลย ทั้งที่ในเชิงธุรกิจมันคนละความหมายกันโดยสิ้นเชิง และความสับสนแบบนี้อาจนำไปสู่บั๊กร้ายแรงที่ตรวจจับได้ยากมากในภายหลัง

ช่องว่างจาก Library ภายนอกที่ Type ไม่สมบูรณ์
ระบบนิเวศ npm เต็มไปด้วย package จำนวนมหาศาลที่เขียนด้วย JavaScript ล้วนๆ โดยไม่มี type definition ที่แม่นยำมาแต่ต้น แม้จะมีโครงการชุมชนอย่าง DefinitelyTyped ที่ช่วยเติมเต็มช่องว่างนี้ แต่ type definition ที่เขียนขึ้นภายหลังก็ไม่ได้รับประกันว่าจะตรงกับพฤติกรรมจริงของ library เสมอไป จึงเกิดช่องว่างระหว่าง "สิ่งที่ TypeScript ยืนยันว่าปลอดภัย" กับ "สิ่งที่เกิดขึ้นจริงเมื่อรันโค้ด"

เหตุผลที่การตรวจสอบตอนรันไทม์กลายเป็นสิ่งจำเป็น
จากข้อจำกัดทั้งหมดที่กล่าวมา จะเห็นได้ชัดว่าจุดอ่อนที่ใหญ่ที่สุดของ TypeScript คือการไม่มีกลไกตรวจสอบใดๆ หลงเหลืออยู่เมื่อโค้ดทำงานจริง นี่คือสาเหตุที่วงการ JavaScript เริ่มให้ความสำคัญกับแนวคิด runtime validation ควบคู่ไปกับ static typing มากขึ้นอย่างต่อเนื่อง

แนวคิดหลักคือการเขียน schema เพื่ออธิบายโครงสร้างข้อมูลเพียงครั้งเดียว แล้วให้ library รับหน้าที่ทั้งตรวจสอบข้อมูลจริงตอนรันไทม์ พร้อมสร้าง TypeScript type ให้โดยอัตโนมัติผ่านการ infer จาก schema เดียวกันนั้น วิธีนี้ช่วยขจัดปัญหาการเขียน type ซ้ำซ้อนสองรอบ ทั้งสำหรับ TypeScript และสำหรับ validation logic ซึ่งเป็นต้นตอของบั๊กที่เกิดจากความไม่สอดคล้องกันระหว่างสิ่งที่ประกาศไว้กับสิ่งที่ตรวจสอบจริง

เครื่องมือรุ่นใหม่ที่กำลังเปลี่ยนเกม

Zod: มาตรฐานที่ทุกคนคุ้นเคย
Zod ครองตำแหน่ง schema validation library ที่ได้รับความนิยมสูงสุดในระบบนิเวศ TypeScript อยู่ในปัจจุบัน ด้วย API ที่เข้าใจง่ายและความสามารถ infer TypeScript type จาก schema โดยอัตโนมัติผ่านฟังก์ชัน z.infer ทำให้ไม่ต้องเขียน type ซ้ำสองรอบอีกต่อไป รองรับการตรวจสอบข้อมูลที่ซับซ้อนอย่าง union type, discriminated union และ custom refinement เหมาะสำหรับการตรวจสอบข้อมูลจาก API ฟอร์ม หรือ environment variable

Valibot: เบาและเร็วกว่าด้วยสถาปัตยกรรม Modular
Valibot ถูกออกแบบมาเพื่อแก้ปัญหาขนาด bundle ที่ใหญ่เกินไปของ validation library รุ่นก่อนหน้า ด้วยสถาปัตยกรรม modular ที่รองรับ tree-shaking อย่างเต็มรูปแบบ เหมาะอย่างยิ่งสำหรับแอปพลิเคชันที่ต้องคำนึงถึงขนาดไฟล์เป็นพิเศษ เช่น เว็บที่ต้องโหลดเร็วมากหรือแอปมือถือแบบ hybrid ที่ไม่อยากแบกโค้ดส่วนที่ไม่ได้ใช้

ArkType: Syntax ที่ใกล้เคียง TypeScript ที่สุด
ArkType นำเสนอแนวทางที่ต่างออกไปด้วยการให้เขียน schema ในรูปแบบที่ใกล้เคียงกับ syntax ของ TypeScript type เองมากที่สุด ผ่าน string literal syntax ที่กระชับอ่านง่าย พร้อมประสิทธิภาพการ validate ที่รวดเร็วกว่า library อื่นในหลายกรณี เพราะใช้เทคนิค compile schema ให้กลายเป็นโค้ดที่ optimize ไว้ล่วงหน้า

Effect Schema: สำหรับสาย Functional Programming
สำหรับทีมที่ใช้ Effect เป็นแกนหลักในการจัดการ side effect และ functional programming อยู่แล้ว Effect Schema คือส่วนขยายที่ผสาน schema validation เข้ากับระบบ error handling และ dependency injection ของ Effect ได้อย่างกลมกลืน เหมาะกับโปรเจกต์ที่ต้องการความแม่นยำสูงในการจัดการ edge case ที่ซับซ้อน

Standard Schema: ก้าวสำคัญสู่มาตรฐานกลาง
หนึ่งในความเคลื่อนไหวที่น่าจับตามากที่สุดคือความพยายามสร้างมาตรฐานกลางที่เรียกว่า Standard Schema ซึ่งเกิดจากความร่วมมือของผู้พัฒนา library ชั้นนำหลายราย รวมถึง Zod, Valibot และ ArkType เพื่อกำหนด interface กลางที่ทำให้ library เหล่านี้ทำงานร่วมกันได้ในระบบเดียว เช่น framework หรือ form library สามารถรองรับ schema จาก library ใดก็ได้โดยไม่ต้อง lock-in กับตัวใดตัวหนึ่ง ความเคลื่อนไหวนี้สะท้อนว่าวงการเริ่มมองว่า runtime validation ไม่ใช่ทางเลือกเสริมอีกต่อไป แต่เป็นโครงสร้างพื้นฐานที่จำเป็นเทียบเท่ากับ static typing

TypeScript เองก็ไม่ได้หยุดพัฒนา
ในขณะเดียวกัน ทีมพัฒนา TypeScript ก็ยังคงเดินหน้าปรับปรุงภาษาอย่างต่อเนื่อง เช่น คำสั่ง satisfies ที่ช่วยตรวจสอบว่าค่าตรงตาม type ที่กำหนดโดยไม่ทำให้เสีย type ที่แคบกว่าไป การพัฒนาความสามารถด้าน type narrowing ให้ฉลาดขึ้นเรื่อยๆ รวมถึงข้อเสนอระดับ TC39 ที่ผลักดันให้ JavaScript รองรับ type annotation ได้โดยตรงในตัวภาษาผ่านแนวคิด "type stripping" ซึ่งจะช่วยให้ runtime อย่าง Node.js และ Deno รันไฟล์ TypeScript ได้ทันทีโดยไม่ต้องผ่านขั้นตอน build แยกต่างหากอีกต่อไปในอนาคต

แนวปฏิบัติที่ทีมพัฒนาควรนำไปใช้จริง
ข้อสรุปสำคัญที่สุดคือ TypeScript และ runtime validation ไม่ใช่คู่แข่งที่ต้องเลือกอย่างใดอย่างหนึ่ง แต่เป็นเครื่องมือที่ควรใช้ร่วมกันเพื่อปิดช่องว่างของกันและกันอย่างสมบูรณ์

ใช้ TypeScript เป็นหลักในการตรวจสอบความถูกต้องของ logic ภายในโค้ดเบสตอน compile time เพื่อจับบั๊กได้เร็วและเพิ่มความเร็วในการพัฒนาผ่าน autocomplete และการ refactor ที่ปลอดภัยขึ้น

ใช้ schema validation library อย่าง Zod หรือ Valibot ตรวจสอบข้อมูลทุกจุดที่ข้ามขอบเขตความไว้วางใจ ไม่ว่าจะเป็น response จาก API ภายนอก ข้อมูลจากฟอร์มผู้ใช้ environment variable หรือข้อมูลจากฐานข้อมูลที่อาจมีโครงสร้างไม่ตรงกับที่คาดไว้เสมอ

พิจารณาใช้ branded type หรือ nominal typing pattern สำหรับค่าที่มีความหมายทางธุรกิจเฉพาะเจาะจง เพื่อป้องกันการสลับใช้ type ที่หน้าตาเหมือนกันแต่ความหมายต่างกันโดยไม่ตั้งใจ

ตั้งค่า compiler ให้เข้มงวดที่สุดผ่าน strict mode พร้อมจำกัดการใช้ any ให้เหลือน้อยที่สุด และพิจารณาใช้ ESLint rule เพื่อป้องกันการใช้ any โดยไม่มีเหตุผลชัดเจนรองรับ

บทสรุป
TypeScript เปลี่ยนโฉมหน้าการพัฒนา JavaScript ไปอย่างสิ้นเชิง และยังคงเป็นเครื่องมือที่จำเป็นสำหรับการสร้างแอปพลิเคชันขนาดใหญ่ในปัจจุบันอย่างไม่ต้องสงสัย แต่การเข้าใจข้อจำกัดของมันอย่างถ่องแท้ โดยเฉพาะธรรมชาติของ type erasure ที่ทำให้ไม่มีการตรวจสอบใดๆ หลงเหลืออยู่ตอนรันไทม์ คือจุดเริ่มต้นสำคัญที่จะช่วยให้ทีมพัฒนาสร้างระบบที่ปลอดภัยจากบั๊กได้อย่างแท้จริง การผสาน TypeScript เข้ากับเครื่องมือ runtime validation อย่าง Zod, Valibot หรือ ArkType พร้อมติดตามความเคลื่อนไหวใหม่ๆ อย่าง Standard Schema คือทิศทางที่วงการกำลังมุ่งหน้าไป และเป็นสิ่งที่นักพัฒนาทุกคนควรเริ่มทำความเข้าใจตั้งแต่วันนี้ ก่อนที่ช่องว่างระหว่าง type ที่ compiler ยืนยันว่าปลอดภัย กับความเป็นจริงตอนรันไทม์ จะกลายเป็นต้นตอของปัญหาที่แก้ไขได้ยากในระบบของคุณเอง

สำหรับธุรกิจที่ต้องการมีแอปพลิเคชันเป็นของตัวเองเพื่อเพิ่มยอดขาย สร้างฐานลูกค้า และยกระดับการให้บริการ เราขอแนะนำ Appsmez ผู้ให้บริการรับทำแอปมือถือและพัฒนาแอปสำหรับธุรกิจ ทั้งระบบ iOS และ Android มีให้เลือกทั้งแบบแอปสำเร็จรูปพร้อมใช้งานและพัฒนาแบบ Custom ตามความต้องการ เหมาะสำหรับธุรกิจ E-Commerce, ร้านอาหาร, ระบบสมาชิก, ระบบจอง และธุรกิจทุกขนาด ดูรายละเอียดเพิ่มเติมได้ที่ https://appsmez.com/

Top comments (0)