Functions
การ type function
หัวข้อที่มีชื่อว่า “การ type function”function type อธิบาย parameter และ return type เขียน inline หรือเป็น named type ก็ได้:
type BinaryOp = (a: number, b: number) => number;
const add: BinaryOp = (a, b) => a + b;// a and b are inferred as number from the context — no annotations neededparameter รองรับ optional (?), default value และ rest:
function make(name: string, count = 1, ...tags: string[]): void { // count defaults to 1; tags collects the rest into string[]}optional parameter x?: number เป็น number | undefined; defaulted parameter count = 1 มองจากฝั่งภายในเป็น number เฉย ๆ แต่ผู้เรียกละไว้ได้
Overload — และทำไม union มักดีกว่า
หัวข้อที่มีชื่อว่า “Overload — และทำไม union มักดีกว่า”overload ให้ function หนึ่งตัวแสดง call signature หลายแบบ เขียน signature หลายบรรทัดตามด้วย implementation หนึ่งอัน:
function len(x: string): number;function len(x: unknown[]): number;function len(x: string | unknown[]): number { return x.length;}
len("hi"); // ✅ 2len([1, 2, 3]); // ✅ 3overload ทรงพลังแต่ใช้เกินง่าย เมื่อ signature ต่างกันแค่ที่ type ของ parameter — ไม่ใช่ที่ ความสัมพันธ์ ระหว่าง parameter กับ return — union parameter ตัวเดียวเรียบง่ายและชัดกว่า:
// Simpler than two overloads:function len(x: string | unknown[]): number { return x.length;}หยิบ overload จริง ๆ มาใช้เฉพาะเมื่อ return type ขึ้นกับ input shape ที่รับเข้ามาในแบบที่ union แสดงไม่ได้ — ซึ่งตรงกับกรณีที่ union parameter สร้างไม่ได้พอดี:
function makeElement(tag: "a"): HTMLAnchorElement;function makeElement(tag: "img"): HTMLImageElement;function makeElement(tag: string): HTMLElement { return document.createElement(tag);}
const link = makeElement("a");link.href = "https://example.com"; // ✅ only exists on HTMLAnchorElement -- overloads know it's that specific type// @expect-error TS2339function makeElementUnion(tag: "a" | "img"): HTMLAnchorElement | HTMLImageElement { return document.createElement(tag);}const linkU = makeElementUnion("a");linkU.href = "https://example.com"; // ❌ still a union -- the compiler can't tell which member you got// Property 'href' does not exist on type 'HTMLAnchorElement | HTMLImageElement'.union version เสียการเชื่อมโยงระหว่าง literal ที่ส่งเข้าไปกับ member ที่ได้กลับมา — return type เป็น union เต็มเสมอ ถึงคุณจะรู้ตรง call site ว่ามันต้องเป็น HTMLAnchorElement แน่ ๆ overload มีไว้เพื่อสิ่งนี้พอดี: เชื่อม input เฉพาะเจาะจงหนึ่งตัวเข้ากับ output เฉพาะเจาะจงหนึ่งตัว ซึ่ง union signature เดียวสร้างไม่ได้
Parameter bivariance กับ strictFunctionTypes
หัวข้อที่มีชื่อว่า “Parameter bivariance กับ strictFunctionTypes”parameter ของ function ปกติเป็น contravariant: function ที่รับ type กว้างกว่า assign แทนที่จุดที่ต้องการ function ที่รับ type แคบกว่า ได้อย่างปลอดภัย (มันรับได้ทุกอย่างที่ตัวแคบกว่ารับได้ และมากกว่านั้น) การ assign กลับทางกัน — function ที่รับแคบกว่าไปแทนที่จุดที่ต้องการตัวกว้างกว่า — ควรจะ unsound เพราะ caller อาจส่งอะไรบางอย่างที่ function แคบกว่าจัดการไม่ได้
strictFunctionTypes (ส่วนหนึ่งของ strict) บังคับกฎ contravariance นั้นเป๊ะ ๆ — แต่เฉพาะ function ที่เขียนเป็น property ของ type (...) => ... function ที่เขียนด้วย method shorthand syntax ได้ข้อยกเว้นที่ตั้งใจไว้เรียกว่า bivariance เก็บไว้เพื่อ backward compatibility กับ pattern อย่าง array method และการ override generic แบบเก่า:
interface Animal { name: string }interface Dog extends Animal { breed: string }
interface ShelterMethod { handle(a: Animal): void; // method shorthand syntax}interface ShelterProp { handle: (a: Animal) => void; // property function syntax}
const byMethod: ShelterMethod = { handle(a: Dog) { console.log(a.breed); }, // narrower param -- unsound if a plain Animal shows up};// @expect-error TS2322interface Animal { name: string }interface Dog extends Animal { breed: string }
interface ShelterProp { handle: (a: Animal) => void; // property function syntax}
const byProp: ShelterProp = { handle: (a: Dog) => { console.log(a.breed); }, // same narrowing, property syntax};// ❌ Types of parameters 'a' and 'a' are incompatible.// Property 'breed' is missing in type 'Animal' but required in type 'Dog'.ทั้งสองการ assign narrow parameter จาก Animal เป็น Dog เหมือนกัน — unsound เท่ากัน เพราะ ShelterMethod/ShelterProp สัญญาว่า handle ของมันรับ Animal ตัวไหนก็ได้ และ Animal ธรรมดาไม่มี breed byMethod compile ผ่านสะอาดใต้ --strict เพราะเขียนด้วย method shorthand; byProp fail ด้วย TS2322 เพราะเขียนเป็น property ที่เก็บ function value ปิด strictFunctionTypes (--strictFunctionTypes false) ทำให้ byProp compile ผ่านด้วย — flag นี้ไม่ได้เพิ่ม check ใหม่ มันแค่หยุดไม่ให้ข้อยกเว้นนั้นทำงาน
Method shorthand vs property function under strictFunctionTypes
interface Animal { name: string }interface Dog extends Animal { breed: string }
interface ShelterMethod { handle(a: Animal): void; // method shorthand -- bivariant, narrowing the param compiles}interface ShelterProp { handle: (a: Animal) => void; // property function -- checked contravariantly under strict}
const byMethod: ShelterMethod = { handle(a: Dog) { console.log(a.breed); }, // compiles even though it's unsound};
// const byProp: ShelterProp = {// handle: (a: Dog) => { console.log(a.breed); }, // would be TS2322 under strict// }; เลือกใช้ method shorthand เฉพาะเมื่อคุณต้องการ covariant-style overriding จริง ๆ (พบบ่อยใน class hierarchy ที่มี generic method); สำหรับ callback property ธรรมดา แบบ property-function ที่เช็คเข้มกว่ามักเป็นสิ่งที่คุณต้องการ เพราะมันจับ unsoundness จริงที่แบบ shorthand ปล่อยผ่านแบบเงียบ ๆ
this: void: สัญญาว่า callback จะไม่แตะ this
หัวข้อที่มีชื่อว่า “this: void: สัญญาว่า callback จะไม่แตะ this”callback parameter ที่ type เป็น this: void คือสัญญาว่า function จะไม่เคยอ่าน this เลย — ซึ่งสำคัญเพราะ method ที่แตะ this จริง ๆ assign เข้าช่องนั้นไม่ปลอดภัย:
// @expect-error TS2322interface Button { onClick: (this: void, event: string) => void }class Widget { label = "save"; handleClick(this: Widget, event: string) { console.log(this.label, event); }}const w = new Widget();const btn: Button = { onClick: w.handleClick };// ❌ The 'this' types of each signature are incompatible.// Type 'void' is not assignable to type 'Widget'.handleClick ต้องการ Widget จริง ๆ ผูกเป็น this ถึงจะทำงานได้ แต่ onClick สัญญาว่า caller เรียกมันด้วย this เป็นอะไรก็ได้ (รวมถึงไม่มีเลย ในฐานะ function reference เปล่า ๆ) ซึ่งตรงกับสิ่งที่เกิดขึ้นพอดีเมื่อ framework เรียก btn.onClick(event) แบบ callback ที่หลุดออกมา ทางแก้คือใช้ arrow function (ที่ capture this แบบ lexical ไม่ต้องมีใครส่งมาให้) หรือเขียน handler เองด้วย this: void แล้วอ่าน state ที่ capture ไว้แทน this
การ type this
หัวข้อที่มีชื่อว่า “การ type this”ใน standalone function this type ได้ด้วย parameter ตัวแรกพิเศษที่ หายไปที่ call site (เป็น compile-time เท่านั้น):
interface Button { label: string; }
function handleClick(this: Button, event: Event): void { console.log(this.label); // this is typed as Button}ช่วยจับบั๊กคลาสสิกที่ method เสีย this เมื่อถูกส่งเป็น callback เปล่า ๆ มีประโยชน์สุดกับ API แบบเก่าที่พึ่ง this; กับ arrow function และ pattern สมัยใหม่คุณจะใช้น้อยลง
กฎ void return ที่ทำทุกคนงง
หัวข้อที่มีชื่อว่า “กฎ void return ที่ทำทุกคนงง”function type ที่ return void ไม่ได้ แปลว่า “function ต้องไม่ return อะไร” แต่แปลว่า “ผู้เรียกจะเมิน return value” function ที่ return อะไรบางอย่างจึงยัง assign ให้ type ที่ return void ได้:
type Callback = () => void;
const cb: Callback = () => 42; // ✅ allowed! the 42 is just ignoredflowchart LR fn["() => number (returns 42)"] -->|assignable to| target["() => void (caller ignores return)"] target --> use["callers treat result as void — safe"]
นี่แหละคือเหตุผลว่าทำไม array.forEach(x => arr.push(x)) ถึง type-check ผ่านทั้งที่ push return number: forEach ต้องการ callback แบบ void และเมินสิ่งที่ return กลับมา พอรู้กฎแล้วก็เลิกลึกลับ — แต่แทบทุกคนต้องงงกับเรื่องนี้ก่อน
สิ่งที่ checker จับได้แล้วตอนนี้ (TypeScript 5.7–5.8)
หัวข้อที่มีชื่อว่า “สิ่งที่ checker จับได้แล้วตอนนี้ (TypeScript 5.7–5.8)”สาม check ใหม่ที่เปลี่ยนวิธี type-check function โดยไม่มี syntax ใหม่:
- ตัวแปรที่ไม่เคยถูก initialize (5.7)
letที่ประกาศ type ไว้แต่ไม่เคย assign เป็น error แม้จุดที่ใช้จะอยู่ใน nested function - แต่ละ branch ของ conditional ที่ return ถูกเช็คแยก (5.8) เมื่อก่อน
anyฝั่งหนึ่งของcond ? a : bกลบ type ผิดของอีกฝั่ง ตอนนี้ทุก branch ต้องเข้ากับ return type ที่ประกาศ - callback ที่ return แค่
nullหรือundefinedp.catch(() => null)infer return type ของ handler จากสิ่งที่คุณเขียน ผลลัพธ์ที่ await ได้จึง widen เป็นT | null— ไม่ error ตรง call site แต่เป็นค่าที่คุณต้อง narrow ก่อนใช้
ตัวแปรที่ไม่เคย initialize:
// @expect-error TS2454function report() { let result: number; // declared, never assigned function print() { console.log(result); // ❌ Variable 'result' is used before being assigned }}แต่ละ branch ของ conditional ถูกเช็ค:
// @expect-error TS2322declare const cache: Map<any, any>;function getUrl(s: string): URL { return cache.has(s) ? cache.get(s) : s; // ^ ❌ Type 'string' is not assignable to type 'URL'}callback ที่ return แค่ null/undefined:
// @expect-error TS18047async function getNumber(): Promise<number> { throw new Error("boom");}
async function run() { const n = await getNumber().catch(() => null); // n: number | null n.toFixed(2); // ❌ 'n' is possibly 'null'}และย้ำจากบท narrowing: ตั้งแต่ 5.5 function ที่ return boolean ซึ่งผูกกับ parameter จะได้ inferred type predicate guard เล็ก ๆ หลายตัวจึงไม่ต้องเขียน x is T อีก
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”flowchart TD sig["A function's type is its promise: what it needs in, what it needs from this, what comes out"] --> params["Parameter promise (checked contravariantly -- but bivariantly for method shorthand)"] sig --> thisP["this promise (this: void means 'never reads this')"] sig --> ret["Return promise (void means 'caller may ignore it')"] params --> when["Union parameter: one promise for every input Overload: a different promise per input shape"]
ทุก feature ในบทนี้คือ compiler เช็คสัญญาด้านหนึ่งตรง boundary ของ function parameter type สัญญาว่า function เรียกด้วยอะไรได้บ้าง; strictFunctionTypes คือระดับความเข้มงวดของการเช็คสัญญานั้น (ยกเว้นข้อยกเว้น bivariant ของ method shorthand) this type สัญญาว่า function อ่านอะไรจาก receiver — this: void สัญญาว่ามันไม่อ่านอะไรเลย return type สัญญาว่า caller ได้อะไรกลับมา — void สัญญาแบบพิเศษว่า caller ไม่สนใจ overload มีไว้เพราะบางครั้งสัญญาเดียวจับความสัมพันธ์ระหว่างสิ่งที่เข้ามากับสิ่งที่ออกไปไม่ได้ นั่นคือตอนที่คุณเขียนหลายสัญญาแทนที่จะเป็นสัญญาเดียว
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | ได้อะไร | เสียอะไร |
|---|---|---|
| Union parameter | signature เดียว อ่านง่าย implement ง่าย | แสดง “input shape นี้บอกเป็นนัยถึง output shape เฉพาะนั้น” ไม่ได้ |
| Overload | เชื่อม input เฉพาะเจาะจงเข้ากับ output เฉพาะเจาะจง | มี declaration ให้ maintain มากขึ้น ใช้เกินง่ายในกรณีที่ union ก็จัดการได้ |
Method shorthand (handle(a: T)) | ตรงกับ syntax method ของ JS; รองรับ covariant-style overriding ใน class hierarchy | Bivariant — ปล่อยผ่านการ narrow parameter ที่ unsound แบบเงียบ ๆ |
Property function (handle: (a: T) => void) | เช็คแบบ contravariant ใต้ strictFunctionTypes — จับ unsoundness จริง | มี ceremony เพิ่มนิดหน่อย; override แบบ class method ไม่ได้ |
this: void | บันทึกและบังคับว่า callback ไม่พึ่ง binding เลย | ต้องเขียน handler ที่ต้องการ this จริง ๆ ใหม่ |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- method ที่เขียนด้วย shorthand syntax (
handle(a: T)) กับ property ที่เก็บ arrow function (handle: (a: T) => void) ถูกเช็คเหมือนกัน ไม่จริง — method shorthand ได้ข้อยกเว้น bivariant เลยยอมรับการ narrow parameter ที่ unsound ที่ property form ปฏิเสธถูกต้องใต้strictFunctionTypes strictFunctionTypesเพิ่ม error แบบใหม่ ไม่จริง — มันไม่ได้เพิ่ม check ใหม่ มันแค่เอาข้อยกเว้น (bivariance) ออกเฉพาะสำหรับ property-function form คืนค่าการเช็ค contravariant ปกติกลับมา- การเก็บ class method เป็น bare callback (
const fn = obj.method) ถูกจับโดย type checker ถ้า method ต้องการthisโดยทั่วไปไม่ถูกจับ เว้นแต่ target signature ประกาศthistype ที่เข้ากันไม่ได้ตรง ๆ — assign เข้าช่อง() => voidธรรมดา (ที่มีthisแบบ implicit ไม่จำกัด) compile ผ่านแม้ method จะพึ่ง receiver จริง - overload เป็นแค่ generic “แบบเก่า” และควรถูกแทนที่เสมอ union parameter เรียบง่ายกว่าเมื่อ signature ต่างกันแค่ type ของ input แต่ overload ยังเป็นเครื่องมือที่ถูกต้องเมื่อ input shape เฉพาะต้องสัญญา output shape เฉพาะที่ต่างกันจริง ๆ
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ก่อนเชื่อ function signature ที่ generate มา ให้เช็คสิ่งเหล่านี้:
- interface member แบบ method shorthand (
handle(a: Base): void) ที่ implement ด้วย parameter type แคบกว่า (handle(a: Derived) { ... }) — อันนี้ compile ผ่านแบบเงียบ ๆ เพราะ bivariance แม้จะ unsound; narrowing แบบเดียวกันบน function ที่ type เป็น property จะ fail ถูกต้อง - class method ที่ถูก assign เข้า callback type ธรรมดาหรือ array ของ callback (
onClick: fnหรือhandlers.push(obj.method)) โดยไม่ bind (.bind(obj)) หรือห่อด้วย arrow (() => obj.method()) — ถ้า method อ่านthisอันนี้ compile ผ่านแล้ว crash ตอนเรียกครั้งแรก - overload สองตัวขึ้นไปที่แทนด้วย union parameter ตัวเดียวได้โดยไม่เสียความแม่นยำ — สัญญาณของ boilerplate ที่ copy-paste มากกว่าความสัมพันธ์ input-to-output ที่แท้จริง
- callback parameter ที่ควรเป็น
this: void(handler ที่หลุดออกมาจริง ๆ) แต่ไม่ได้ type ไว้ — ไม่มีอะไรกัน function ที่พึ่งthisไม่ให้ถูกส่งเข้าไปตรงนั้นแทน
🐛 หา bug ในโค้ดนี้
class Counter {count = 0;increment(this: Counter) { this.count++; }}
const c = new Counter();const handlers: Array<() => void> = [];handlers.push(c.increment);
for (const h of handlers) h();เฉลย
โค้ดนี้ compile ผ่านโดยไม่มี error เลย increment ประกาศ this: Counter แต่ Array<() => void> ต้องการ function ที่มี this แบบ implicit ไม่จำกัดของ () => void — TypeScript เช็คความเข้ากันได้ของ this ก็ต่อเมื่อ signature เป้าหมาย ประกาศ this type ไว้เอง ซึ่งที่นี่ไม่ได้ประกาศ c.increment เลยหลุดเข้าไปใน array ในฐานะ function reference เปล่า ๆ ที่ไม่ผูกอะไร ตอน runtime การเรียก h() เรียก increment โดยมี this เป็น undefined (class body แบบ strict-mode ไม่ default this เป็น global object) เลยทำให้ this.count++ throw TypeError: Cannot read properties of undefined (reading 'count') ทางแก้คือเก็บ reference ที่ bind ไว้แล้ว — handlers.push(c.increment.bind(c)) หรือ handlers.push(() => c.increment()) เพื่อให้ receiver เดินทางไปพร้อมกับ function