ข้ามไปยังเนื้อหา

Functions

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 needed

parameter รองรับ 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 ให้ 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"); // ✅ 2
len([1, 2, 3]); // ✅ 3

overload ทรงพลังแต่ใช้เกินง่าย เมื่อ 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 TS2339
function 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 ของ 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 TS2322
interface 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
// };
Open in TS Playground

เลือกใช้ method shorthand เฉพาะเมื่อคุณต้องการ covariant-style overriding จริง ๆ (พบบ่อยใน class hierarchy ที่มี generic method); สำหรับ callback property ธรรมดา แบบ property-function ที่เช็คเข้มกว่ามักเป็นสิ่งที่คุณต้องการ เพราะมันจับ unsoundness จริงที่แบบ shorthand ปล่อยผ่านแบบเงียบ ๆ

callback parameter ที่ type เป็น this: void คือสัญญาว่า function จะไม่เคยอ่าน this เลย — ซึ่งสำคัญเพราะ method ที่แตะ this จริง ๆ assign เข้าช่องนั้นไม่ปลอดภัย:

// @expect-error TS2322
interface 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

ใน 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 สมัยใหม่คุณจะใช้น้อยลง

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 ignored
flowchart LR
  fn["() => number
(returns 42)"] -->|assignable to| target["() => void
(caller ignores return)"]
  target --> use["callers treat result
as void — safe"]
void แปลว่า return ถูกเมิน ไม่ใช่ห้าม return

นี่แหละคือเหตุผลว่าทำไม array.forEach(x => arr.push(x)) ถึง type-check ผ่านทั้งที่ push return number: forEach ต้องการ callback แบบ void และเมินสิ่งที่ return กลับมา พอรู้กฎแล้วก็เลิกลึกลับ — แต่แทบทุกคนต้องงงกับเรื่องนี้ก่อน

สาม 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 หรือ undefined p.catch(() => null) infer return type ของ handler จากสิ่งที่คุณเขียน ผลลัพธ์ที่ await ได้จึง widen เป็น T | null — ไม่ error ตรง call site แต่เป็นค่าที่คุณต้อง narrow ก่อนใช้

ตัวแปรที่ไม่เคย initialize:

// @expect-error TS2454
function report() {
let result: number; // declared, never assigned
function print() {
console.log(result); // ❌ Variable 'result' is used before being assigned
}
}

แต่ละ branch ของ conditional ถูกเช็ค:

// @expect-error TS2322
declare 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 TS18047
async 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 อีก

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"]
ทุกกฎเรื่อง type function คือสัญญาที่ compiler เช็คตรง boundary

ทุก feature ในบทนี้คือ compiler เช็คสัญญาด้านหนึ่งตรง boundary ของ function parameter type สัญญาว่า function เรียกด้วยอะไรได้บ้าง; strictFunctionTypes คือระดับความเข้มงวดของการเช็คสัญญานั้น (ยกเว้นข้อยกเว้น bivariant ของ method shorthand) this type สัญญาว่า function อ่านอะไรจาก receiver — this: void สัญญาว่ามันไม่อ่านอะไรเลย return type สัญญาว่า caller ได้อะไรกลับมา — void สัญญาแบบพิเศษว่า caller ไม่สนใจ overload มีไว้เพราะบางครั้งสัญญาเดียวจับความสัมพันธ์ระหว่างสิ่งที่เข้ามากับสิ่งที่ออกไปไม่ได้ นั่นคือตอนที่คุณเขียนหลายสัญญาแทนที่จะเป็นสัญญาเดียว

ตัวเลือกได้อะไรเสียอะไร
Union parametersignature เดียว อ่านง่าย implement ง่ายแสดง “input shape นี้บอกเป็นนัยถึง output shape เฉพาะนั้น” ไม่ได้
Overloadเชื่อม input เฉพาะเจาะจงเข้ากับ output เฉพาะเจาะจงมี declaration ให้ maintain มากขึ้น ใช้เกินง่ายในกรณีที่ union ก็จัดการได้
Method shorthand (handle(a: T))ตรงกับ syntax method ของ JS; รองรับ covariant-style overriding ใน class hierarchyBivariant — ปล่อยผ่านการ 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 ประกาศ this type ที่เข้ากันไม่ได้ตรง ๆ — assign เข้าช่อง () => void ธรรมดา (ที่มี this แบบ implicit ไม่จำกัด) compile ผ่านแม้ method จะพึ่ง receiver จริง
  • overload เป็นแค่ generic “แบบเก่า” และควรถูกแทนที่เสมอ union parameter เรียบง่ายกว่าเมื่อ signature ต่างกันแค่ type ของ input แต่ overload ยังเป็นเครื่องมือที่ถูกต้องเมื่อ input shape เฉพาะต้องสัญญา output shape เฉพาะที่ต่างกันจริง ๆ

ก่อนเชื่อ 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

ควรใช้ overload แทน union parameter ตัวเดียวเมื่อไร?
this parameter (function f(this: T, ...)) ทำอะไรที่ call site?
const cb: () => void = () => 42 ทำได้ไหม?
ทำไม arr.forEach(x => other.push(x)) ถึง type-check ผ่านทั้งที่ push return number?
ใต้ --strict ตัวไหน compile ผ่าน?
interface ShelterMethod {
  handle(a: Animal): void;
}
interface ShelterProp {
  handle: (a: Animal) => void;
}

const byMethod: ShelterMethod = {
  handle(a: Dog) { console.log(a.breed); },
};
const byProp: ShelterProp = {
  handle: (a: Dog) => { console.log(a.breed); },
};
โค้ดนี้ compile ผ่านไหม แล้วเกิดอะไรขึ้นตอนรัน?
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();