Skip to content

บทที่ 03 — Functions (ฟังก์ชันและ Overload)

← บทที่ 02 | สารบัญ | บทที่ 04: Object Types →

⏱ ใช้เวลา 2–3 ชั่วโมง


บทนี้เรียนอะไร

ฟังก์ชันคือ "หน่วยการทำงาน" ที่เราใช้บ่อยที่สุดในการเขียนโปรแกรม การใส่ type ให้ฟังก์ชันถูกต้องคือทักษะที่สำคัญที่สุดอย่างหนึ่งของ TypeScript เพราะ ฟังก์ชันคือ "สัญญา" — มันบอกว่า "ส่งอะไรเข้ามาได้บ้าง และจะได้อะไรกลับไป"

บทนี้จะค่อย ๆ ไต่จาก:

  • การใส่ type ให้ฟังก์ชันพื้นฐาน
  • พารามิเตอร์แบบต่าง ๆ — ไม่บังคับ, มีค่าเริ่มต้น, รับได้ไม่จำกัด
  • การตั้งชื่อ type ให้ฟังก์ชัน
  • Function Overload — ฟังก์ชันที่มีหลายหน้าตา
  • ฟังก์ชันขั้นสูง — this, higher-order function, async

Part 1: ใส่ type ให้ฟังก์ชันพื้นฐาน

1.1 ส่วนประกอบของฟังก์ชันที่ต้องใส่ type

ฟังก์ชันหนึ่งตัวมี "จุดที่ต้องใส่ type" อยู่ 2 จุด:

typescript
function add(a: number, b: number): number {
    //         ↑──────────────────┘  ↑──────┘
    //         1. type ของพารามิเตอร์   2. type ของค่าที่คืน
    return a + b;
}
  • type ของพารามิเตอร์ — บอกว่า "รับอะไรเข้ามาได้" — จุดนี้ ต้องใส่เสมอ (ถ้าเปิด strict TypeScript จะบังคับ)
  • type ของค่าที่คืน (return type) — บอกว่า "คืนอะไรกลับไป" — จุดนี้ใส่หรือไม่ใส่ก็ได้ (อ่านต่อใน 1.3)

1.2 ฟังก์ชันแบบ arrow ก็ใส่ type เหมือนกัน

JavaScript เขียนฟังก์ชันได้หลายแบบ ทุกแบบใส่ type ที่ตำแหน่งเดียวกัน:

typescript
// แบบประกาศปกติ (function declaration)
function add(a: number, b: number): number {
    return a + b;
}

// แบบ arrow function เก็บใน const
const subtract = (a: number, b: number): number => a - b;

// arrow function ที่ไม่คืนค่า
const sayHi = (name: string): void => {
    console.log(`Hi, ${name}`);
};

1.3 Return type — ใส่หรือไม่ใส่ดี?

ถ้าเราไม่เขียน return type TypeScript จะ เดาให้เอง (inference) จากค่าที่ return:

typescript
function add(a: number, b: number) {
    return a + b;   // TypeScript เดาเองว่า return type คือ number
}

แล้วควรเขียนเองหรือปล่อยให้เดา? นี่คือคำแนะนำ:

ปล่อยให้เดาได้ — สำหรับฟังก์ชันเล็ก ๆ ภายในไฟล์ ที่ดูปุ๊บรู้ปั๊บว่าคืนอะไร

ควรเขียนเอง — ในกรณีเหล่านี้:

  • ฟังก์ชันที่เป็น "API สาธารณะ" ให้คนอื่นเรียกใช้ — เขียนไว้เพื่อความชัดเจน
  • ฟังก์ชันยาว ๆ ซับซ้อน — เขียน return type ช่วยให้ TypeScript จับได้ถ้าเราเผลอ return ผิดประเภท

ลองดูประโยชน์ของการเขียน return type เอง:

typescript
// เขียน return type เป็น number ไว้
function calculate(x: number): number {
    if (x > 0) {
        return x * 2;
    }
    // ❌ Error! Function lacks ending return statement
    // TypeScript จับได้ว่าเราลืม return กรณี x <= 0
}

ถ้าไม่เขียน return type ไว้ TypeScript จะเดา return type เป็น number | undefined แล้วเงียบ ๆ ปล่อยผ่าน — การเขียน return type เองจึงเป็น "ตาข่ายนิรภัย" อีกชั้น

🎯 คำแนะนำสำหรับมือใหม่: ช่วงแรกเขียน return type เองทุกฟังก์ชันไปก่อน จะได้ฝึกคิดว่า "ฟังก์ชันนี้คืนอะไร" พอชำนาญแล้วค่อยเลือกปล่อยให้เดาในจุดที่ชัดเจน


Part 2: พารามิเตอร์แบบต่าง ๆ

JavaScript มีวิธีรับพารามิเตอร์หลายแบบ มาดูว่า TypeScript ใส่ type ให้แต่ละแบบยังไง

2.1 พารามิเตอร์ไม่บังคับ (Optional) — ใส่ ?

ปกติพารามิเตอร์ทุกตัว "ต้องส่ง" ถ้าไม่ส่ง TypeScript จะ error แต่บางพารามิเตอร์เราอยากให้ "ส่งหรือไม่ส่งก็ได้" — ใส่ ? หลังชื่อ:

typescript
function greet(name: string, greeting?: string): string {
    //                              ↑ ? = ไม่บังคับ
    if (greeting === undefined) {
        return `Hi, ${name}`;
    }
    return `${greeting}, ${name}`;
}

greet("Alice");              // ✅ ไม่ส่ง greeting ก็ได้
greet("Alice", "Hello");     // ✅ ส่งก็ได้

⚠️ สำคัญ: เมื่อใส่ ? พารามิเตอร์นั้นจะมี type เป็น string | undefined โดยอัตโนมัติ — เพราะถ้าคนไม่ส่ง ค่ามันจะเป็น undefined ดังนั้นภายในฟังก์ชันคุณ ต้องเช็ก undefined ก่อนใช้เสมอ

⚠️ กฎตำแหน่ง: พารามิเตอร์ไม่บังคับต้องอยู่ "หลังสุด" เสมอ — จะเอามาไว้กลางไม่ได้:

typescript
function bad(greeting?: string, name: string) {}
// ❌ Error ตอน compile! พารามิเตอร์บังคับ (name) ห้ามอยู่หลังพารามิเตอร์ไม่บังคับ
// (เป็น TypeScript compile-time error — โค้ดนี้จะ compile ไม่ผ่าน ไม่ใช่ error ตอน runtime)

(เหตุผล: ถ้าทำได้ TypeScript จะไม่รู้ว่าค่าที่ส่งมาตัวแรกหมายถึงตัวไหน)

2.2 พารามิเตอร์มีค่าเริ่มต้น (Default)

แทนที่จะให้เป็น undefined เมื่อไม่ส่ง เราตั้ง "ค่าเริ่มต้น" ได้:

typescript
function greet(name: string, greeting: string = "Hi"): string {
    //                                          ↑ ค่าเริ่มต้น
    return `${greeting}, ${name}`;
}

greet("Alice");              // "Hi, Alice"      ← ใช้ค่าเริ่มต้น
greet("Alice", "Hello");     // "Hello, Alice"   ← ใช้ค่าที่ส่งมา

ข้อดีกว่า optional: ภายในฟังก์ชัน greeting จะเป็น string เสมอ (ไม่ใช่ string | undefined) เพราะถ้าไม่ส่งก็ได้ค่าเริ่มต้นไปแล้ว — ไม่ต้องเช็ก undefined

สังเกตว่าเราไม่ต้องเขียน : string ให้ greeting ด้วยซ้ำ — TypeScript เดา type จากค่าเริ่มต้น "Hi" ได้เอง

Optional vs Default — เลือกอันไหน?

  • ถ้ามี "ค่าที่สมเหตุสมผลเมื่อไม่ส่ง" → ใช้ default (สะดวกกว่า ไม่ต้องเช็ก undefined)
  • ถ้า "การไม่ส่ง" มีความหมายต่างจากการส่งค่าใด ๆ → ใช้ optional

2.3 พารามิเตอร์รับได้ไม่จำกัด (Rest) — ใช้ ...

บางฟังก์ชันต้องรับพารามิเตอร์ "กี่ตัวก็ได้" เช่น ฟังก์ชันบวกเลข — ใช้ ...:

typescript
function sum(...numbers: number[]): number {
    //       ↑ ... รวมพารามิเตอร์ที่เหลือทั้งหมดเป็น array
    return numbers.reduce((total, n) => total + n, 0);
}

sum(1, 2);              // 3
sum(1, 2, 3, 4, 5);     // 15
sum();                  // 0

ภายในฟังก์ชัน numbers จะเป็น number[] (array) — TypeScript รวมพารามิเตอร์ทุกตัวที่ส่งมาเป็น array ให้

⚠️ rest parameter ต้องเป็นพารามิเตอร์ "ตัวสุดท้าย" เสมอ

2.4 พารามิเตอร์แบบ Destructure (แตก object)

เมื่อฟังก์ชันรับ object ที่มีหลาย field เรามักใช้เทคนิค "destructure" — แตก field ออกมาเป็นตัวแปร การใส่ type ให้แบบนี้มือใหม่มักงง มาดูให้ชัด:

typescript
function createUser(options: { name: string; age: number; isAdmin?: boolean }) {
    console.log(options.name);
}

แบบข้างบนเขียน type รวมไว้ในวงเล็บปีกกา ถ้าอยากแตก field ออกมา:

typescript
function createUser({ name, age, isAdmin = false }: {
    name: string;
    age: number;
    isAdmin?: boolean;
}) {
    console.log(name, age, isAdmin);
}

สังเกตว่า type อยู่ หลัง : ของทั้งก้อน destructure — ไม่ใช่แทรกในแต่ละตัวแปร

แต่เขียนแบบนี้รก ทางที่ดีกว่าคือ ตั้งชื่อ type แยกออกมา:

typescript
type CreateUserOptions = {
    name: string;
    age: number;
    isAdmin?: boolean;
};

function createUser({ name, age, isAdmin = false }: CreateUserOptions) {
    console.log(name, age, isAdmin);
}

createUser({ name: "Alice", age: 30 });
createUser({ name: "Bob", age: 25, isAdmin: true });

💡 เทคนิคที่นิยม: เมื่อฟังก์ชันมีพารามิเตอร์เยอะ (เกิน 2-3 ตัว) ให้รวมเป็น object เดียวที่ตั้งชื่อ type ไว้ ข้อดี: คนเรียกไม่ต้องจำลำดับ, เพิ่มพารามิเตอร์ใหม่ทีหลังง่าย, โค้ดอ่านง่ายเพราะมีชื่อ field กำกับ


Part 3: ตั้งชื่อ type ให้ฟังก์ชัน

บางครั้งเราอยาก "ตั้งชื่อ" ให้รูปแบบของฟังก์ชัน เพื่อนำกลับมาใช้ซ้ำ มี 2 วิธี

3.1 Function Type Alias

ใช้ type เขียนรูปร่างของฟังก์ชัน:

typescript
type BinaryOperation = (a: number, b: number) => number;
//                      ↑ พารามิเตอร์            ↑ return type (ใช้ => ไม่ใช่ :)

สังเกต — ในการ "เขียน type ของฟังก์ชัน" เราใช้ => คั่นระหว่างพารามิเตอร์กับ return type (ต่างจากตอนเขียนฟังก์ชันจริงที่ใช้ :)

นำไปใช้:

typescript
type BinaryOperation = (a: number, b: number) => number;

const add: BinaryOperation = (a, b) => a + b;
const multiply: BinaryOperation = (a, b) => a * b;

สังเกตว่าตอนเขียน add เราไม่ต้องใส่ : number ให้ a และ b แล้ว — เพราะ TypeScript รู้จาก BinaryOperation อยู่แล้วว่าพารามิเตอร์เป็นอะไร นี่เรียกว่า contextual typing

💡 Contextual typing = TypeScript เดา type ของ callback หรือตัวแปร จากบริบทที่ใช้งาน — เช่น ตัวอย่างข้างบน TypeScript รู้ว่า a และ b ต้องเป็น number เพราะดูจาก BinaryOperation ที่ประกาศไว้ ตัวอย่างอีกอัน: callback ของ addEventListener("click", ...) — TypeScript รู้ว่าพารามิเตอร์ต้องเป็น MouseEvent เพราะดูจาก method ที่ส่ง callback เข้าไป

ประโยชน์: ถ้ามีฟังก์ชันหลายตัวที่หน้าตาเหมือนกัน ตั้งชื่อ type ไว้ครั้งเดียว ใช้ซ้ำได้หมด

3.2 Call Signature (ลายเซ็นการเรียกใช้ — เขียนด้วย interface)

อีกวิธี — เขียนรูปร่างฟังก์ชันด้วย interface:

typescript
interface BinaryOperation {
    (a: number, b: number): number;
}

ผลลัพธ์เหมือน type alias ทุกประการ ความต่างคือ — interface แบบนี้ทำได้มากกว่า มันสามารถมี "property อื่น" ติดมาด้วย (ฟังก์ชันใน JavaScript ก็เป็น object ที่มี property ได้):

typescript
interface Counter {
    (): number;          // เรียกแบบฟังก์ชันได้
    count: number;       // และมี property count
    reset(): void;       // และมี method reset
}

แบบนี้คือ "object ที่เรียกเหมือนฟังก์ชันได้" — ใช้ในกรณีเฉพาะ ตอนนี้แค่รู้ว่ามีก็พอ


Part 4: void — ฟังก์ชันที่ไม่คืนค่า

เราเจอ void มาบ้างแล้วในบทที่ 02 ในบริบทของฟังก์ชัน void หมายถึง "ฟังก์ชันนี้ไม่คืนค่าที่มีความหมาย":

typescript
function logError(message: string): void {
    console.error(message);
    // ไม่มี return ที่มีค่า
}

ฟังก์ชันที่ทำหน้าที่ "ก่อผลข้างเคียง" (พิมพ์ออกจอ, บันทึกข้อมูล, ส่ง notification) มักมี return type เป็น void

มีจุดที่มือใหม่งงเรื่อง void — เมื่อ void เป็น type ของ callback มันหมายถึง "ค่าที่คืนจะถูกเพิกเฉย" ไม่ได้แปลว่า "ห้ามคืนค่า":

typescript
type Callback = () => void;

// callback นี้คืนค่า number แต่ TypeScript ยอม เพราะ void = "ไม่สนค่าที่คืน"
const cb: Callback = () => 42;

ลองดูตัวอย่างที่เจอจริง:

typescript
const numbers: number[] = [];

// array.push() คืน length ใหม่ของ array (เป็น number)
// แต่ forEach ต้องการ callback แบบ () => void — ถ้า "void = ห้ามคืนค่า" จริง
// บรรทัดข้างล่างจะ error เพราะ push คืน number
[1, 2, 3].forEach(n => numbers.push(n));   // ✅ ไม่ error — void = "ไม่สนค่าที่คืน"

นี่คือเหตุผลที่เราเขียน array.forEach(x => doSomething(x)) ได้ แม้ doSomething จะคืนค่าอะไรมา — forEach ต้องการ callback แบบ void ซึ่งแปลว่า "คืนอะไรมาก็ได้ ฉันไม่สน"

⚠️ กฎนี้ไม่ได้พิเศษเฉพาะ forEach — มันคือกฎทั่วไปของ TypeScript: ทุกที่ที่ต้องการฟังก์ชันชนิด return void (ไม่ว่าจะเป็นพารามิเตอร์ของ method ใน array หรือแค่ตัวแปรธรรมดาที่ประกาศ type เป็น () => void) จะยอมรับฟังก์ชันที่คืนค่าอะไรก็ได้เสมอ ไม่ใช่แค่กรณี forEach เท่านั้น


Part 5: Function Overload (โอเวอร์โหลด — ฟังก์ชันหลายหน้าตา)

นี่คือหัวข้อหลักของบท และเป็นหัวข้อที่มือใหม่มักไม่เข้าใจ ขออธิบายให้ละเอียด

5.1 ปัญหาที่ Overload แก้

บางครั้งฟังก์ชันหนึ่งตัว เรา "อยากให้มันรับ input ได้หลายแบบ และ output เปลี่ยนตาม input" ลองดูปัญหานี้:

typescript
// เราอยากได้ฟังก์ชันที่:
// - ถ้าส่ง number มา → คืน number
// - ถ้าส่ง string มา → คืน string

ถ้าใช้ union ธรรมดา จะได้แบบนี้:

typescript
function double(x: number | string): number | string {
    if (typeof x === "number") return x * 2;
    return x + x;
}

const a = double(5);       // type ของ a คือ number | string  ← ปัญหา!

ปัญหาคือ — เรารู้อยู่แล้วว่าส่ง 5 (number) เข้าไป ก็ต้องได้ number กลับมาแน่ ๆ

แต่ TypeScript กลับบอกว่า a มี type เป็น number | string

ผลคือเราต้องมา narrow (เช็ก type) ซ้ำอีกครั้งทั้งที่ไม่จำเป็น — พูดง่าย ๆ คือ TypeScript "เสียข้อมูล" ความสัมพันธ์ระหว่าง input กับ output ไป

5.2 ทางแก้ — เขียน Overload Signature

Overload คือการเขียน "หน้าตาของฟังก์ชันหลายแบบ" แยกกัน แล้วตามด้วยการ implement จริงหนึ่งครั้ง:

typescript
// --- Overload signatures (หน้าตาที่คนเรียกเห็น) ---
function double(x: number): number;
function double(x: string): string;

// --- Implementation signature (การทำงานจริง — คนเรียกไม่เห็นอันนี้) ---
function double(x: number | string): number | string {
    if (typeof x === "number") return x * 2;
    return x + x;
}

const a = double(5);       // type: number  ✅ ถูกต้อง!
const b = double("ab");    // type: string  ✅ ถูกต้อง!
const c = double(true);    // ❌ Error! ไม่มี overload ที่รับ boolean

อธิบายโครงสร้าง — overload มี 3 ส่วน:

  1. Overload signatures (2 บรรทัดบน) — เป็นแค่ "ประกาศหน้าตา" ไม่มี body ({}) — นี่คือสิ่งที่คนเรียกใช้มองเห็นและเลือกได้
  2. Implementation signature — บรรทัดที่มี body จริง ต้อง "ครอบคลุมทุก overload" ได้ (จึงใช้ number | string)
  3. ⚠️ Implementation signature ไม่ถูกนับเป็นตัวเลือก — คนเรียกจะเห็นแค่ 2 overload ด้านบน ไม่เห็นบรรทัด implementation

🚨 จุดสำคัญที่มือใหม่พลาด: Implementation signature เรียกจากภายนอกไม่ได้ แม้ type จะดูกว้างกว่า:

typescript
// ใช้ overload ข้างบน — เห็นแค่ double(x: number) และ double(x: string)
const x: number | string = Math.random() > 0.5 ? 5 : "ab";
double(x);   // ❌ Error! ไม่มี overload ไหนรับ number | string

เหตุผล: implementation signature เป็นแค่ "วิธีเขียน body ให้ครอบทุกกรณี" ไม่ใช่ public API คนเรียกจากภายนอกเห็นแค่ overload signatures ด้านบน ถ้าอยากให้รับ number | string ได้ต้องเพิ่ม overload signature อีกตัวอย่างชัดเจน

5.3 ตัวอย่างจริง — overload ใน DOM API

ฟังก์ชัน document.querySelector ที่คุณใช้บ่อยก็เป็น overload — ลองดูว่าเขาออกแบบยังไง:

typescript
function select(tag: "img"): HTMLImageElement | null;
function select(tag: "input"): HTMLInputElement | null;
function select(tag: string): HTMLElement | null;
function select(tag: string): HTMLElement | null {
    return document.querySelector(tag);
}

const img = select("img");     // type: HTMLImageElement | null
const inp = select("input");   // type: HTMLInputElement | null
const div = select("div");     // type: HTMLElement | null (ใช้ overload ตัวสุดท้าย)

เห็นไหมว่า overload ทำให้ TypeScript "ฉลาด" — ส่ง "img" ได้ type ของ <img> กลับมาเป๊ะ ไม่ต้อง assert เอง

📌 เกร็ดเล็ก: ใน DOM API จริง document.querySelector ไม่ได้ใช้ overload เขียนทีละบรรทัดแบบนี้ — มันใช้เทคนิคชื่อ HTMLElementTagNameMap (object type ที่ map ชื่อ tag ไปเป็น type ของ element) ร่วมกับ generic ทำให้ครอบคลุมทุก tag ในไฟล์เดียวจบ คุณยังไม่ต้องเข้าใจตอนนี้ว่าทำไมมันดีกว่า — แค่รู้ไว้ว่ามีเทคนิคแบบนี้อยู่ ค่อยไปเรียนละเอียดในบทที่ 05 (Generics)

5.4 เมื่อไหร่ใช้ Overload เมื่อไหร่ใช้ Union

  • ใช้ Overload — เมื่อ input กับ output "สัมพันธ์กัน" (input แบบนี้ → output แบบนี้)
  • ใช้ Union ธรรมดา — เมื่อ input หลายแบบ แต่ output เหมือนกันหมด ไม่ขึ้นกับ input
typescript
// output เหมือนกันหมด (string) ไม่ว่า input เป็นอะไร → union พอ ไม่ต้อง overload
function stringify(x: number | boolean): string {
    return String(x);
}

💡 Overload เขียนยุ่งกว่า ใช้เมื่อจำเป็นจริง ๆ — ในงานสมัยใหม่ หลายกรณีที่เคยใช้ overload ปัจจุบันใช้ generic แทนได้สวยกว่า (เรียนในบทที่ 05)


Part 6: this Parameter — กำหนดบริบทของ this

ใน JavaScript คำว่า this ในฟังก์ชันมีพฤติกรรมซับซ้อน TypeScript ช่วยได้ด้วยการให้เราระบุ "type ของ this" เป็นพารามิเตอร์พิเศษตัวแรก:

typescript
function describe(this: { name: string }): string {
    return `ฉันชื่อ ${this.name}`;
}

const person = { name: "Alice", describe };
person.describe();              // ✅ this คือ person ซึ่งมี name

const empty = { describe };
empty.describe();               // ❌ Error! empty ไม่มี name

พารามิเตอร์ this พิเศษตรงที่ — มันไม่ใช่พารามิเตอร์จริง ตอนเรียกใช้ไม่ต้องส่งค่าให้ มันมีไว้บอก TypeScript เท่านั้นว่า "ฟังก์ชันนี้ต้องถูกเรียกในบริบทที่ this มีรูปร่างแบบนี้" — และตอน compile มันจะหายไป

หัวข้อนี้ใช้ไม่บ่อยในโค้ดสมัยใหม่ (เพราะ arrow function และ class จัดการ this ให้แล้ว) — รู้ไว้ว่ามีก็พอ


Part 7: Higher-Order Function (ฟังก์ชันลำดับสูง — รับ/คืนฟังก์ชัน)

"Higher-order function" คือฟังก์ชันที่ รับฟังก์ชันเป็นพารามิเตอร์ หรือ คืนฟังก์ชันกลับมา คุณใช้มันอยู่แล้วโดยไม่รู้ตัว — array.map(), array.filter() ล้วนเป็น higher-order function

7.1 รับฟังก์ชันเป็นพารามิเตอร์

typescript
function repeat(times: number, action: (index: number) => void): void {
    //                          ↑ พารามิเตอร์ที่เป็นฟังก์ชัน
    for (let i = 0; i < times; i++) {
        action(i);
    }
}

repeat(3, (i) => console.log(`รอบที่ ${i}`));

สังเกตว่า type ของพารามิเตอร์ action คือ (index: number) => void — เราเขียน type ของฟังก์ชันได้ตรง ๆ ในตำแหน่งพารามิเตอร์

7.2 คืนฟังก์ชันกลับมา

typescript
function makeMultiplier(factor: number): (n: number) => number {
    //                                   ↑ return type เป็นฟังก์ชัน
    return (n) => n * factor;
}

const triple = makeMultiplier(3);
triple(10);    // 30

makeMultiplier คืนฟังก์ชันใหม่กลับมา — return type จึงเป็น (n: number) => number


Part 8: Async Function — ฟังก์ชันที่ทำงานแบบ asynchronous

⚠️ ต้องรู้ก่อน: Part นี้สมมติว่าคุณเข้าใจ Promise และ async/await จากฝั่ง JavaScript แล้ว — ถ้ายังไม่เคยเรียน ให้อ่าน JavaScript บทที่ 09 — Async/Await ก่อน แล้วค่อยกลับมา TypeScript แค่เพิ่ม type ทับลงไปบนกลไกเดิม

สรุปสั้น ๆ แบบกันลืม — Promise คือ "กล่องที่หุ้มค่าที่ยังไม่มาตอนนี้ แต่จะมาในอนาคต" (เช่น ผลลัพธ์จากการเรียก API ที่ต้องรอเน็ตเวิร์กตอบกลับ) ส่วน async คือคำที่ติดหน้าฟังก์ชัน บอกว่า "ฟังก์ชันนี้ข้างในมีจุดที่ต้องหยุดรอ" และทำให้ใช้ await ได้ — await แปลว่า "หยุดตรงนี้ก่อน รอจนกว่า Promise จะได้ค่าจริง ค่อยไปทำบรรทัดถัดไป" ถ้ายังไม่คุ้นกลไกนี้ แนะนำอ่านบทที่อ้างถึงข้างบนให้ละเอียดก่อน เพราะที่นี่จะพูดถึงเฉพาะ "ใส่ type ยังไง" ไม่ลงลึกกลไก await เอง

ฟังก์ชัน async คือฟังก์ชันที่ทำงานกับสิ่งที่ "ต้องรอ" เช่น การเรียก API การคืนค่าของ async function จะถูก "ห่อ" ด้วย Promise เสมอ

typescript
async function fetchUserName(id: number): Promise<string> {
    //                                    ↑ return type ต้องห่อด้วย Promise<>
    const response = await fetch(`/api/users/${id}`);
    const user = await response.json();
    return user.name;   // เราคืน string ธรรมดา
}

จุดที่มือใหม่งง: ในฟังก์ชันเราเขียน return user.name (เป็น string ธรรมดา) แต่ return type ต้องเขียนเป็น Promise<string>ทำไม?

เพราะธรรมชาติของ async — ฟังก์ชัน async จะห่อค่าที่คืนด้วย Promise ให้อัตโนมัติเสมอ คุณ return string แต่คนที่เรียกฟังก์ชันนี้จะได้ Promise<string> กลับไป (ต้อง await เพื่อแกะเอา string ออกมา)

กฎจำง่าย: return type ของ async function = Promise<ชนิดของค่าที่ return จริง>

typescript
async function getNumber(): Promise<number> { return 42; }
async function getNothing(): Promise<void> { console.log("done"); }

ถ้าไม่เขียน return type เอง TypeScript ก็เดา Promise<...> ให้ถูกอยู่แล้ว — แต่การเขียนเองช่วยให้อ่านง่ายและกันพลาด

📌 หากยังไม่แม่นเรื่อง Promise / async / await แนะนำกลับไปทบทวนบท async ใน Javascript ก่อน — TypeScript แค่เพิ่ม type ทับลงไป กลไกการทำงานเหมือน JavaScript ทุกอย่าง


Part 9: Generator (รู้จักไว้พอ)

function* (มี *) คือ "generator" — ฟังก์ชันที่ "ทยอยคืนค่าทีละตัว" ใช้ไม่บ่อยในงานทั่วไป รู้จักหน้าตาไว้ก็พอ:

typescript
function* countTo(max: number): Generator<number> {
    for (let i = 1; i <= max; i++) {
        yield i;   // ทยอยคืนค่าทีละตัว
    }
}

for (const n of countTo(5)) {
    console.log(n);   // 1, 2, 3, 4, 5
}

return type เป็น Generator<number> — หมายถึง "generator ที่ทยอยคืน number" ถ้าคุณยังไม่เคยใช้ generator มาก่อน ข้ามไปได้ ค่อยกลับมาเมื่อจำเป็น

📝 syntax Generator<number> ที่มี <> คือ generic (ตัวแปร type — บอกว่า generator นี้ทยอยคืนค่าประเภทอะไร) ตอนนี้ดูแล้วงงเป็นธรรมดา เราเรียนเรื่อง generic เต็ม ๆ ในบทที่ 05


Part 10: typeof — ดึง type ออกจากฟังก์ชันที่มีอยู่

บางครั้งเรามีฟังก์ชันอยู่แล้ว และอยากได้ "type ของมัน" มาใช้ — ใช้ typeof:

typescript
function formatPrice(amount: number, currency: string): string {
    return `${amount} ${currency}`;
}

type FormatPriceFn = typeof formatPrice;
// FormatPriceFn = (amount: number, currency: string) => string

typeof ชื่อฟังก์ชัน ดึง type ของฟังก์ชันนั้นออกมา มีประโยชน์เวลาเราอยากประกาศตัวแปรอื่นให้มี type เดียวกับฟังก์ชันที่มีอยู่ โดยไม่ต้องเขียน type ซ้ำ

(อย่าสับสน — typeof ใน "โลกของ type" แบบนี้ ต่างจาก typeof ที่ใช้เช็กค่าตอน runtime แม้เขียนเหมือนกัน เราจะเจออีกในบทที่ 07)


Part 11: Lab — ลงมือทำ

Lab 1 — พารามิเตอร์แบบ object

ฝึกออกแบบฟังก์ชันที่รับพารามิเตอร์เป็น object เดียว (แทนการรับหลายตัวเรียงกัน) — อ่านง่ายกว่าเมื่อมีหลายค่า และใส่ค่า default + optional ได้สะดวก:

typescript
type SendEmailOptions = {
    to: string;
    subject: string;
    body: string;
    cc?: string[];
    priority?: "low" | "normal" | "high";
};

function sendEmail({ to, subject, body, cc = [], priority = "normal" }: SendEmailOptions): void {
    console.log(`ส่งถึง ${to} | ${subject} | ความสำคัญ: ${priority} | สำเนา: ${cc.length} คน`);
}

sendEmail({ to: "a@x.com", subject: "สวัสดี", body: "..." });
sendEmail({ to: "b@x.com", subject: "ด่วน", body: "...", priority: "high" });

Lab 2 — Overload

ฝึกเขียน function overload — ประกาศ signature หลายแบบให้ฟังก์ชันเดียว เพื่อให้ TypeScript รู้ว่า input ชนิดไหนคืน output ชนิดไหน (string→string[], number→number[]):

typescript
function parseInput(input: string): string[];
function parseInput(input: number): number[];
function parseInput(input: string | number): string[] | number[] {
    if (typeof input === "string") {
        return input.split(",");
    }
    return [input, input * 2, input * 3];
}

const words = parseInput("a,b,c");   // type: string[]
const nums = parseInput(10);         // type: number[]

Lab 3 — Higher-order function

typescript
function createValidator(minLength: number): (value: string) => boolean {
    return (value) => value.length >= minLength;
}

const isValidPassword = createValidator(8);
console.log(isValidPassword("12345"));      // false
console.log(isValidPassword("12345678"));   // true

Part 12: Checkpoint

1. ควรเขียน return type ของฟังก์ชันเองเมื่อไหร่?

ควรเขียนเองเมื่อเป็นฟังก์ชัน API สาธารณะ หรือฟังก์ชันยาว/ซับซ้อน — เพราะ return type ที่เขียนไว้จะช่วยจับกรณีที่เราเผลอ return ผิดประเภทหรือลืม return

2. พารามิเตอร์ optional (?) กับ default ต่างกันยังไง?

optional: ถ้าไม่ส่ง ค่าเป็น undefined (type กลายเป็น T | undefined ต้องเช็กก่อนใช้) default: ถ้าไม่ส่ง ได้ค่าเริ่มต้นที่กำหนด (type ยังเป็น T ปกติ ไม่ต้องเช็ก)

3. Function Overload ใช้ตอนไหน?

ใช้เมื่อ input กับ output ของฟังก์ชัน "สัมพันธ์กัน" — input แบบหนึ่งให้ output แบบหนึ่ง — และเราอยากให้ TypeScript รู้ความสัมพันธ์นี้ ถ้า output เหมือนกันหมดไม่ขึ้นกับ input ใช้ union ธรรมดาพอ

4. Overload signature 3 ส่วนมีอะไรบ้าง?

(1) overload signatures — ประกาศหน้าตาหลายแบบ ไม่มี body (2) implementation signature — มี body จริง ต้องครอบคลุมทุก overload (3) implementation signature ไม่ถูกนับเป็นตัวเลือกที่คนเรียกเห็น

5. ทำไม return type ของ async function ต้องเป็น Promise<T>?

เพราะ async function จะ "ห่อ" ค่าที่ return ด้วย Promise ให้อัตโนมัติเสมอ — เรา return ค่าธรรมดา แต่คนเรียกได้ Promise กลับไป ต้อง await เพื่อแกะค่าออกมา

6. Higher-order function คืออะไร?

ฟังก์ชันที่รับฟังก์ชันเป็นพารามิเตอร์ หรือคืนฟังก์ชันกลับมา (เช่น map, filter)

7. typeof fn (ใน type space) ทำอะไร?

ดึง type ของฟังก์ชัน fn ออกมาเป็น type ใหม่ ใช้นำ type ของฟังก์ชันที่มีอยู่มาใช้ซ้ำโดยไม่ต้องเขียนใหม่


Part 13: สรุปบทนี้

  • ฟังก์ชันมี 2 จุดที่ใส่ type — พารามิเตอร์ (บังคับ) และ return type (เลือกได้)
  • พารามิเตอร์มี 4 แบบ — ปกติ, optional ?, default =, rest ...
  • พารามิเตอร์เยอะ → รวมเป็น object เดียวที่ตั้งชื่อ type
  • ตั้งชื่อ type ฟังก์ชัน ได้ด้วย type alias หรือ call signature (ลายเซ็นการเรียกใช้)
  • Overload — ฟังก์ชันหลายหน้าตา ใช้เมื่อ input/output สัมพันธ์กัน
  • async function — return type ห่อด้วย Promise<T> เสมอ
  • Higher-order function — ฟังก์ชันที่รับ/คืนฟังก์ชัน

บทถัดไป — Object Types และ Interface แบบเจาะลึก


← บทที่ 02 | สารบัญ | บทที่ 04: Object Types →