แนวทางปฏิบัติที่ดีที่สุดในการเขียนโปรแกรม TypeScript
บทความนี้อธิบายแนวทางปฏิบัติที่ดีที่สุดในการเขียนโปรแกรมด้วย TypeScript
คู่มือนี้อธิบายแนวทางปฏิบัติที่ดีที่สุดในการใช้ชนิดข้อมูลของ TypeScript เพื่อลดข้อผิดพลาดและทำให้โค้ดอ่านง่ายขึ้น
YouTube Video
แนวทางปฏิบัติที่ดีที่สุดในการเขียนโปรแกรม TypeScript
ข้อดีที่ใหญ่ที่สุดของ TypeScript คือ 'ป้องกันข้อผิดพลาดด้วยชนิดข้อมูลและทำให้จุดประสงค์ในโค้ดชัดเจน'
แนวทางปฏิบัติที่ดีที่สุดไม่ใช่แค่กฎเท่านั้น แต่คือหลักเกณฑ์ในการเขียนโค้ดให้ปลอดภัย อ่านง่าย และดูแลรักษาได้ ด้านล่างนี้เราจะแนะนำแนวทางปฏิบัติที่ดีที่สุดของ TypeScript ที่ใช้บ่อยพร้อมตัวอย่างในทางปฏิบัติ
หลีกเลี่ยงการใช้ any และควรกำหนดชนิดข้อมูลที่มีความหมายอยู่เสมอ
ก่อนอื่น มาดูประเด็นเรื่อง 'หลีกเลี่ยง any และกำหนดประเภทข้อมูลที่มีความหมาย' กันก่อน
any จะปิดการตรวจสอบชนิดข้อมูลทั้งหมด ซึ่งทำให้ขาดจุดประสงค์ของการใช้ TypeScript แทนที่จะใช้ any เพียงเพื่อให้โค้ดทำงาน ควรกำหนดชนิดข้อมูลให้ละเอียดมากที่สุดเท่าที่ทำได้
1// Bad
2function parse(data: any) {
3 return data.value;
4}โค้ดนี้สามารถรับค่าทุกชนิดได้ จึงไม่สามารถป้องกันข้อผิดพลาดขณะรันได้
1// Good
2type ParsedData = {
3 value: string;
4};
5
6function parse(data: ParsedData): string {
7 return data.value;
8}ด้วยการกำหนดชนิดข้อมูล คุณจะทำให้เจตนาของข้อมูลขาเข้าและขาออกชัดเจนและปลอดภัยยิ่งขึ้น
ให้กำหนดโครงสร้างของอ็อบเจ็กต์ด้วย type หรือ interface อย่างชัดเจนเสมอ
ถัดไป มาดูประเด็นเรื่อง 'การกำหนดโครงสร้างของอ็อบเจ็กต์อย่างชัดเจนเสมอโดยใช้ type หรือ interface'
หากใช้อ็อบเจ็กต์โดยไม่ได้กำหนดแบบแผน โครงสร้างของมันจะไม่ชัดเจน ควรดึงชนิดข้อมูลออกมาเสมอเพื่อการใช้งานซ้ำและดูแลรักษาง่าย
1// Bad
2function createUser(user: { name: string; age: number }) {
3 console.log(user.name);
4}แม้แต่ในโค้ดขนาดเล็ก การสร้างนิสัยแยกประเภทข้อมูลก็เป็นสิ่งสำคัญ
1// Good
2type User = {
3 name: string;
4 age: number;
5};
6
7function createUser(user: User): void {
8 console.log(user.name);
9}การตั้งชื่อชนิดข้อมูลจะช่วยให้เข้าใจโค้ดทั้งโปรเจกต์ได้ง่ายขึ้นมาก
ใช้ union types เพื่อแสดงสถานะที่เป็นไปได้ทั้งหมดอย่างแม่นยำ
หากใช้ string หรือ number แบบดิบในการตรวจเงื่อนไข อาจทำให้ค่าผิดปกติหลุดลอดได้ ด้วยการใช้ union types คุณสามารถแสดงเฉพาะสถานะที่อนุญาตได้ในระดับประเภทข้อมูล
1// Bad
2function setStatus(status: string) {
3 console.log(status);
4}ในโค้ดนี้ สตริงที่ไม่มีความหมายหรือค่าที่ไม่ถูกต้องจะไม่สามารถถูกตรวจจับได้ในช่วงเวลาคอมไพล์
1// Good
2type Status = "idle" | "loading" | "success" | "error";
3
4function setStatus(status: Status): void {
5 console.log(status);
6}ด้วยการใช้ union types คุณสามารถตัดสถานะที่เป็นไปไม่ได้ออกอย่างมั่นใจตั้งแต่ช่วงเวลาคอมไพล์ ผลลัพธ์คือความปลอดภัยของสาขาคำสั่งเงื่อนไขและความน่าเชื่อถือของโค้ดจะเพิ่มขึ้น
จัดการกับ null และ undefined อย่างชัดเจน
ถัดไป มาดูประเด็นเรื่อง 'การจัดการกับ null และ undefined อย่างชัดเจน'
ใน TypeScript สิ่งสำคัญคือต้องระบุในชนิดข้อมูลว่าสามารถไม่มีค่าได้ หากปล่อยให้คลุมเครือ อาจเกิดข้อผิดพลาดขณะรันได้
1type User = {
2 name: string;
3 email?: string;
4};email อาจไม่มีอยู่ ควรเขียนโค้ดโดยสมมติในกรณีนี้ไว้
1function printEmail(user: User): void {
2 if (user.email) {
3 console.log(user.email);
4 }
5}ควรตรวจสอบค่าที่เป็น optional ทุกครั้งก่อนใช้งาน
ใช้ type assertions (as) เฉพาะเมื่อจำเป็นจริงๆ เท่านั้น
ถัดไป มาดูประเด็นเรื่อง 'ไม่ควรใช้ type assertion เกินความจำเป็น'
Type assertions จะข้ามการตรวจสอบประเภทของ TypeScript ชั่วคราวเพื่อประกาศว่า 'ฉันรู้ว่าค่านี้เป็นประเภทนี้' หากใช้มากเกินไปจะทำลายความปลอดภัยของชนิดข้อมูล
1// Bad
2const value = input as string;ในโค้ดนี้ แม้ว่าค่าจริงจะไม่ใช่สตริงก็จะไม่มีข้อผิดพลาดเกิดขึ้น ซึ่งอาจทำให้เกิดข้อผิดพลาดขณะรันโปรแกรมได้ ดังที่แสดงในโค้ดต่อไปนี้ เลือกที่จะตรวจสอบโดยใช้ type guards อย่างปลอดภัยก่อน
1// Good
2function isString(value: unknown): value is string {
3 return typeof value === "string";
4}
5
6if (isString(input)) {
7 console.log(input.toUpperCase());
8}Type guards เป็นกลไกสำหรับตรวจสอบประเภทอย่างปลอดภัยขณะตรวจสอบค่าจริง ด้วยการให้ความสำคัญกับ type guards มากกว่า type assertions คุณจะสามารถป้องกันข้อผิดพลาดขณะรันโปรแกรมได้ง่ายขึ้น
อย่าพึ่งพา type inference สำหรับชนิดข้อมูลที่คืนค่ามากเกินไป
ถัดไป มาดูประเด็นเรื่อง 'ไม่ควรพึ่งพาการอนุมานประเภทผลลัพธ์มากเกินไป'
การอนุมานชนิดข้อมูลของ TypeScript นั้นดีมาก แต่ควรระบุชนิดคืนค่าของฟังก์ชันสาธารณะให้ชัดเจนจะปลอดภัยกว่า จะช่วยลดผลกระทบหากมีการเปลี่ยนแปลงในอนาคต
1// Bad
2function sum(a: number, b: number) {
3 return a + b;
4}ระบุเจตนาให้ชัดเจนถึงแม้ในฟังก์ชันขนาดเล็ก
1// Good
2function sum(a: number, b: number): number {
3 return a + b;
4}การระบุชนิดคืนค่าช่วยเพิ่มความเสถียรให้กับ API
จัดการข้อมูลขาเข้าอย่างปลอดภัยโดยใช้ unknown
ถัดไป มาดูประเด็นเรื่อง 'การรับข้อมูลจากภายนอกอย่างปลอดภัยด้วยการใช้ unknown'
สำหรับอินพุตภายนอก เช่น APIs, JSON หรืออินพุตจากผู้ใช้ ควรใช้ unknown แทน any ด้วยวิธีนี้ คุณจะมั่นใจได้ว่าค่าทั้งหมดได้รับการตรวจสอบ เพื่อรักษาความปลอดภัยของประเภทข้อมูล
1// Bad
2function handleResponse(data: any) {
3 console.log(data.id);
4}นี่คือวิธีการตรวจสอบชนิดข้อมูลด้วย unknown
1// Good
2function handleResponse(data: unknown): void {
3 if (
4 typeof data === "object" &&
5 data !== null &&
6 "id" in data
7 ) {
8 console.log((data as { id: number }).id);
9 }
10}unknown ไม่สามารถใช้งานโดยตรงได้ แต่เป็นประเภทที่ต้องมีการตรวจสอบก่อนใช้งาน วิธีนี้มีประสิทธิภาพเป็นพิเศษเมื่อใช้กับการจัดการอินพุตภายนอก
เพิ่มความยืดหยุ่นของโค้ดด้วยการรวมชนิดข้อมูลขนาดเล็ก
ถัดไป มาดูประเด็นเรื่อง 'เพิ่มความสามารถในการแสดงออกด้วยการรวมประเภทข้อมูลขนาดเล็กเข้าด้วยกัน'
การกำหนดชนิดข้อมูลขนาดใหญ่ทีเดียวจะลดความอ่านง่ายและการดูแลรักษา แบ่งชนิดข้อมูลออกเป็นหน่วยที่มีความหมาย และนำมารวมกันเมื่อจำเป็น
1type Id = number;
2
3type UserProfile = {
4 id: Id;
5 name: string;
6};
7
8type UserWithStatus = UserProfile & {
9 status: "active" | "inactive";
10};การมองชนิดข้อมูลเป็นส่วนประกอบย่อยช่วยจัดโครงสร้างการออกแบบ
type และ interface
ข้อดีของ interface
type และ interface สามารถใช้กำหนดชนิดข้อมูลได้ทั้งคู่แต่จุดประสงค์และคุณสมบัติแตกต่างกัน การใช้แต่ละแบบกับจุดประสงค์ที่เหมาะสมจะช่วยให้ความตั้งใจของการกำหนดชนิดข้อมูลชัดเจนยิ่งขึ้น
1// Bad
2type User = {
3 id: number;
4 name: string;
5};
6
7type AdminUser = {
8 id: number;
9 name: string;
10 role: "admin";
11};หากคุณทำซ้ำส่วนที่เหมือนกันแบบนี้ โค้ดจะเปราะบางต่อการเปลี่ยนแปลง
1// Good
2interface User {
3 id: number;
4 name: string;
5}
6
7interface AdminUser extends User {
8 role: "admin";
9}interface เหมาะกับดีไซน์ที่ต้องมีการขยาย (extends) และเหมาะกับการอธิบาย 'โครงร่าง' ของอ็อบเจ็กต์
ข้อดีของ type
ในทางกลับกัน type จะมีความยืดหยุ่นมากกว่า เหมาะกับการจัดการ union และ intersection types
1// Good
2type Status = "idle" | "loading" | "success" | "error";
3
4type ApiResponse<T> =
5 | { status: "success"; data: T }
6 | { status: "error"; message: string };type เหมาะสำหรับแสดงสถานะ ตัวเลือก และการรวมชนิดข้อมูลหลายแบบ
หลักในการเลือกใช้ระหว่าง type และ interface
โดยหลักทั่วไป ให้ใช้ interface เมื่อกำหนดโครงสร้างอ็อบเจ็กต์หรือสัญญา และใช้ type เมื่อจำเป็นต้องรวม union, intersection หรือดำเนินการกับชนิดข้อมูล
ทั้งสองมีการทำงานคล้ายกัน แต่ควรเลือกใช้ให้สื่อให้เห็นจุดประสงค์การมีอยู่ของชนิดข้อมูลนั้น
พิจารณาให้ชนิดข้อมูลเป็นเอกสารประกอบโค้ด
สุดท้าย มาดูประเด็นเรื่อง 'การเขียนประเภทข้อมูลให้เป็นเสมือนเอกสารประกอบโปรแกรม'
การกำหนดชนิดข้อมูลที่ดีจะให้ข้อมูลมากกว่าคำอธิบายโค้ด สิ่งสำคัญคือการมุ่งสู่สภาวะที่ 'สามารถเข้าใจสเปคของโปรแกรมเพียงแค่ดูประเภทข้อมูล'
1type ApiError = {
2 code: number;
3 message: string;
4 retryable: boolean;
5};ด้วยเหตุนี้ จุดเด่นข้อหนึ่งของ TypeScript คือการที่นิยามประเภทข้อมูลสามารถทำหน้าที่เสมือนเป็นเอกสารสเปคของโปรแกรมได้
สรุป
แนวทางปฏิบัติที่ดีของ TypeScript ไม่ใช่การเคร่งครัดเกินไป หัวใจสำคัญคือ ชี้แจงเจตนาผ่านชนิดข้อมูลและเขียนโค้ดให้ยืดหยุ่นต่อการเปลี่ยนแปลง
ด้วยการสะสมกฎเล็กๆ ในการพัฒนาแต่ละวัน จะเห็นผลระยะยาว เช่น 'รีวิวโค้ดง่ายขึ้น' 'มีบั๊กน้อยลง' และ 'เข้าใจง่ายขึ้นทั้งต่อตนเองในอนาคตและผู้อื่น'
อันดับแรก เมื่อคิดว่า 'จะสื่อสิ่งนี้ด้วยชนิดข้อมูลอย่างไร?' ก็จะสามารถเขียนโค้ดคุณภาพสูงในสไตล์ TypeScript ได้
คุณสามารถติดตามบทความข้างต้นโดยใช้ Visual Studio Code บนช่อง YouTube ของเรา กรุณาตรวจสอบช่อง YouTube ด้วย