แนวทางปฏิบัติที่ดีที่สุดในการเขียนโปรแกรม TypeScript

แนวทางปฏิบัติที่ดีที่สุดในการเขียนโปรแกรม 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 ด้วย

YouTube Video