บทความ › การเขียนโปรแกรม

Temporal ใน JavaScript ใช้งานจริง: บวกเดือน คำนวณอายุ และ polyfill สำหรับ Safari

Temporal เป็น Stage 4 ใช้ได้ใน Chrome 144, Firefox 139 และ Node.js 26 แล้ว โค้ดที่ใช้บ่อยแทน Date ต่อกับโค้ดเดิม แสดง พ.ศ. และโหลด polyfill เฉพาะ Safari

กลาง อ่าน 13 นาที 23 ตุลาคม 2569

อ่านจบแล้วจะรู้
  • ณ ตุลาคม 2026 Temporal ใช้ได้ใน Chrome/Edge 144, Firefox 139 และ Node.js 26 แต่ Safari รุ่น stable ยังไม่มี งานบนเว็บต้องมี polyfill
  • 31 ม.ค. บวก 1 เดือนใน Temporal ได้ 28 ก.พ. ส่วน Date ได้ 3 มี.ค. และสั่งให้ error แทนได้ด้วย overflow: 'reject'
  • until และ since ต้องบอก largestUnit ไม่อย่างนั้นได้หน่วยเล็กสุด เช่น PT94500S แทน 26 ชั่วโมง 15 นาที
  • ห้ามเทียบด้วย < หรือ > ใช้ compare กับ equals เพราะ valueOf ตั้งใจ throw

Date ของ JavaScript ทำงานเดียวได้ดีคือเก็บ instant เป็นมิลลิวินาที ที่เหลือ ทั้งบวกเดือน นับวัน แยกวันในปฏิทินออกจากเวลา ล้วนต้องเขียนเองหรือพึ่ง library Temporal คือ API ใหม่ในภาษาที่แก้เรื่องนี้ และปี 2026 ใช้ได้จริงแล้วใน Chrome, Firefox และ Node.js บทความนี้เป็นโค้ดที่ใช้บ่อย รันจริงทุกบรรทัด

ทดสอบบน Node.js 26.12.1 (Windows 11) และ temporal-polyfill 1.0.6 บน Node.js 20.14 สถานะ browser ตรวจกับ MDN, caniuse และ web.dev ณ ตุลาคม 2026

หลักการเก็บวันเวลา (เก็บ UTC, เก็บ timezone ผู้ใช้, ทำไมบวก 24 ชั่วโมงไม่เท่ากับพรุ่งนี้) อยู่ในบทความ เก็บวันเวลาให้ถูก แล้ว บทนี้ไม่ทวนซ้ำ จะไปต่อที่โค้ดใช้งาน

สถานะ ณ ตุลาคม 2026

ที่รันรองรับตั้งแต่หมายเหตุ
Firefox139browser แรกที่ส่ง Temporal
Chrome / Edge144ออกมกราคม 2026
Node.js26.0.0เปิดเป็นค่าเริ่มต้น ไม่ต้องใส่ flag
Safari (macOS, iOS)ยังไม่มีใน stableมีใน Technology Preview

ผลคือ MDN ยังจัดเป็น “Limited availability” ไม่ใช่ Baseline เพราะ Safari ยังไม่มี งานฝั่ง server บน Node 26 ใช้ได้เลย งานบนเว็บต้องเตรียม polyfill (หัวข้อท้ายบท)

เลือกชนิดตามงาน

Temporal มีหลายชนิด หัวใจคือเลือกให้ตรงความหมายของข้อมูล

ข้อมูลชนิด
เวลาที่เกิดเหตุการณ์ (log, created_at)Temporal.Instant
นัดหมายที่ผูกกับเมืองTemporal.ZonedDateTime
วันเกิด วันหยุด วันครบกำหนดTemporal.PlainDate
เวลาเปิดร้าน 09:00Temporal.PlainTime
รอบบิลรายเดือนTemporal.PlainYearMonth
ระยะเวลาTemporal.Duration

ทุกชนิด แก้ไม่ได้ (immutable) เมธอดอย่าง add คืนค่าตัวใหม่เสมอ บั๊กแบบ setMonth() ที่ไปแก้ object ที่คนอื่นถืออยู่จึงหายไป

ต่อกับโค้ดเดิมที่ใช้ Date

ไม่ต้องรื้อทั้งระบบ แปลงไปกลับได้ตรง ๆ

const d = new Date('2026-10-23T03:30:00Z');
const inst = d.toTemporalInstant();          // Temporal.Instant
inst.toString();                              // '2026-10-23T03:30:00Z'
inst.epochMilliseconds;                       // 1792726200000
new Date(inst.epochMilliseconds);             // กลับเป็น Date

Temporal.Instant.fromEpochMilliseconds(1792726200000).toString();
// '2026-10-23T03:30:00Z'

ใช้ Temporal.Instant กับนาฬิกาเวลาจริงได้ด้วย Temporal.Now.instant() ซึ่งเก็บละเอียดถึงนาโนวินาที (epochNanoseconds เป็น bigint)

บวกเดือน: 31 มกราคม + 1 เดือน

นี่คือบั๊กคลาสสิกของรอบบิลรายเดือน

// Date: แก้ object เดิม และล้นไปเดือนถัดไป
const js = new Date(2026, 0, 31);
js.setMonth(1);                               // 3 มีนาคม 2026

// Temporal: ติดขอบท้ายเดือน
const jan31 = Temporal.PlainDate.from('2026-01-31');
jan31.add({ months: 1 }).toString();          // '2026-02-28'
jan31.add({ months: 1 }, { overflow: 'reject' });
// RangeError: ไม่มีวันที่ 31 กุมภาพันธ์

ไล่ทีละขั้นว่า Date ได้ 3 มีนาคมยังไง: ตั้งเดือนเป็นกุมภาพันธ์โดยวันยังเป็น 31 → กุมภาพันธ์ 2026 มี 28 วัน → เกินมา 3 วัน → ล้นไป 3 มีนาคม ส่วน Temporal ค่าเริ่มต้น overflow: 'constrain' ดึงกลับมาวันสุดท้ายของเดือน ถ้างานนั้นต้องการให้คนตัดสินใจเอง (เช่นสัญญาที่ระบุวันที่ 31) ใช้ 'reject' แล้วจัดการ error

Temporal: add 1 เดือน31 ม.ค. + 1 เดือนติดขอบที่ 28 ก.พ.Date: setMonth(1)31 ม.ค. + 1 เดือนล้นไปเป็น 3 มี.ค.
31 ม.ค. บวกหนึ่งเดือน สองวิธีได้คนละวัน

นับวันและคำนวณอายุ: until กับ largestUnit

const birth = Temporal.PlainDate.from('1990-02-28');
const today = Temporal.PlainDate.from('2026-10-23');
const age = birth.until(today, { largestUnit: 'years' });
age.toString();                     // 'P36Y7M25D'
age.years;                          // 36

const due = Temporal.PlainDate.from('2026-12-31');
today.until(due).days;              // 69
today.until(due, { largestUnit: 'months' }).toString();   // 'P2M8D'

ตรวจด้วยมือ: 1990-02-28 + 36 ปี = 2026-02-28, + 7 เดือน = 2026-09-28, + 25 วัน = 2026-10-23 ตรง ส่วน 69 วัน = 8 วันที่เหลือของตุลาคม + 30 (พฤศจิกายน) + 31 (ธันวาคม)

กับดักคือ ไม่บอก largestUnit ระหว่าง Instant สองตัว ค่าเริ่มต้นเป็นวินาที

const a = Temporal.Instant.from('2026-10-23T03:30:00Z');
const b = Temporal.Instant.from('2026-10-24T05:45:00Z');
a.until(b).toString();                              // 'PT94500S'
a.until(b, { largestUnit: 'hours' }).toString();    // 'PT26H15M'

94,500 ÷ 3,600 = 26.25 ชั่วโมง = 26 ชั่วโมง 15 นาที เอาไปแสดงผลโดยไม่บอกหน่วยจะได้ตัวเลขที่ผู้ใช้อ่านไม่ออก

Duration แปลงหน่วยได้อีกทาง:

const dur = Temporal.Duration.from({ minutes: 130 });
dur.round({ largestUnit: 'hours' }).toString();   // 'PT2H10M'
dur.total('hours');                               // 2.1666666666666665

รอบบิลรายเดือน: PlainYearMonth

รอบบิลไม่ใช่วันที่ แต่เป็น “เดือน” เก็บเป็น PlainYearMonth แล้วแปลงเป็นวันแรกกับวันสุดท้ายตอนต้องใช้ ไม่ต้องจำว่าเดือนไหนมีกี่วัน

const ym = Temporal.PlainYearMonth.from('2026-10');
ym.daysInMonth;                                   // 31
ym.toPlainDate({ day: 1 }).toString();            // '2026-10-01'
ym.toPlainDate({ day: ym.daysInMonth }).toString(); // '2026-10-31'
ym.add({ months: 4 }).toString();                 // '2027-02' ข้ามปีให้เอง

Temporal.PlainYearMonth.from('2028-02').daysInMonth;   // 29 (ปีอธิกสุรทิน)

ฝั่ง browser ใช้ Temporal.Now.timeZoneId() อ่านโซนของผู้ใช้ (เครื่องทดสอบได้ 'Asia/Bangkok') แล้วส่งไปเก็บที่ server คู่กับบัญชีผู้ใช้ จะได้คำนวณ “วันนี้” ของผู้ใช้ฝั่ง server ได้ถูก

เทียบและเรียง: ห้ามใช้ < >

const list = ['2026-11-02', '2026-10-30', '2026-12-01'].map(s => Temporal.PlainDate.from(s));
list.sort(Temporal.PlainDate.compare);     // 2026-10-30, 2026-11-02, 2026-12-01
list[0].equals(Temporal.PlainDate.from('2026-10-30'));   // true
list[0] < list[1];
// TypeError: Do not use Temporal.PlainDate.prototype.valueOf;
//            use Temporal.PlainDate.prototype.compare for comparison.

valueOf ตั้งใจ throw เพื่อกันการเทียบด้วยเครื่องหมายที่ให้ผลผิดเงียบ ๆ === ไม่ throw แต่เทียบว่าเป็น object เดียวกันหรือไม่ ไม่ใช่ค่าเท่ากัน ต้องใช้ equals

ทุกชนิดมี compare แบบ static ส่งเข้า sort ได้ตรง ๆ และใช้ equals แทน === เสมอ

ปัดเวลา: ช่องจองทุก 15 นาที

const t = Temporal.PlainTime.from('09:50');
t.round({ smallestUnit: 'minute', roundingIncrement: 15 }).toString();
// '09:45:00'  (ค่าเริ่มต้น halfExpand: 50 ใกล้ 45 มากกว่า 60)
t.round({ smallestUnit: 'minute', roundingIncrement: 15, roundingMode: 'ceil' }).toString();
// '10:00:00'  (ปัดขึ้น สำหรับช่องจองถัดไปที่ว่าง)

แสดงภาษาไทยและ พ.ศ.

locale th-TH ใช้ปฏิทินพุทธเป็นค่าเริ่มต้นอยู่แล้ว ไม่ต้องบวก 543 เอง

const z = Temporal.ZonedDateTime.from('2026-10-23T10:30[Asia/Bangkok]');
z.toLocaleString('th-TH', { dateStyle: 'long', timeStyle: 'short' });
// '23 ตุลาคม 2569 เวลา 10:30'

today.toLocaleString('th-TH', { calendar: 'buddhist', dateStyle: 'full' });
// 'วันศุกร์ที่ 23 ตุลาคม พ.ศ. 2569'
today.withCalendar('buddhist').year;     // 2569

ข้อมูลที่เก็บยังเป็นปี ค.ศ. ตามปฏิทิน ISO การแปลงเป็น พ.ศ. ทำตอนแสดงเท่านั้น ตามกฎในบทความเรื่องวันเวลาที่ลิงก์ไว้ข้างบน

ส่งผ่าน JSON และ API

JSON.stringify({ due, at: a });
// '{"due":"2026-12-31","at":"2026-10-23T03:30:00Z"}'

toJSON คืน ISO string ฝั่งรับต้อง from() กลับเองเพราะ JSON ไม่รู้ชนิด ให้ตกลงกันในสัญญา API ว่าฟิลด์ไหนเป็น PlainDate (ไม่มีเวลา) ฟิลด์ไหนเป็น Instant (มี Z)

Polyfill สำหรับ Safari

โหลด polyfill เฉพาะเมื่อไม่มีของจริง browser ที่มีแล้วจะไม่ต้องดาวน์โหลดเพิ่ม

npm i temporal-polyfill
if (typeof globalThis.Temporal === 'undefined') {
  await import('temporal-polyfill/global');
}

สคริปต์เดียวกันนี้รันบน Node.js 20.14 (ไม่มี Temporal) ใช้ polyfill และบน Node.js 26.12.1 ใช้ของจริง ได้ผล 2026-02-28 เท่ากัน await ระดับบนสุดต้องอยู่ใน ES module ถ้าใช้ bundler ให้แยก polyfill เป็น chunk ของมันเอง

ข้อผิดพลาดที่เจอบ่อย

  • ใช้ < > หรือ === เทียบค่า ใช้ compare และ equals
  • ลืม largestUnit ได้ PT94500S แทนชั่วโมงและนาที
  • ใช้ PlainDateTime เก็บนัดหมาย ไม่มี timezone ข้ามโซนแล้วผิด ใช้ ZonedDateTime
  • Temporal.Now.plainDateISO() บนเซิร์ฟเวอร์ ได้ “วันนี้” ตาม timezone ของเซิร์ฟเวอร์ ไม่ใช่ของผู้ใช้ ส่งชื่อโซนเข้าไปเสมอ เช่น Temporal.Now.plainDateISO('Asia/Bangkok')
  • เชื่อว่าใช้ได้ทุก browser แล้ว Safari stable ยังไม่มี ต้องมี polyfill และทดสอบบน iPhone จริง
  • บวก 543 เองแล้วเก็บลงฐานข้อมูล ให้ locale ทำตอนแสดงผล

แหล่งอ้างอิง

คำถามที่พบบ่อย

ควรเลิกใช้ Date ทั้งหมดเลยไหม

ไม่ต้องรื้อ โค้ดเก่าที่ใช้ Date เก็บ instant แล้วส่งเป็น ISO string ทำงานถูกอยู่แล้ว เริ่มใช้ Temporal ในจุดที่ Date ทำยาก เช่น บวกเดือน นับวัน คำนวณอายุ หรือนัดหมายข้าม timezone แล้วแปลงไปมาด้วย toTemporalInstant กับ epochMilliseconds

polyfill ตัวไหนดี

มีสองตัวหลักคือ temporal-polyfill (fullcalendar) และ @js-temporal/polyfill (ทีม proposal) ณ ตุลาคม 2026 temporal-polyfill อยู่รุ่น 1.0.6 อัปเดตล่าสุดเดือนนี้ ส่วน @js-temporal/polyfill อยู่ 0.5.1 อัปเดตล่าสุดมีนาคม 2025 บทความนี้ทดสอบตัวแรก

Node.js รุ่นไหนใช้ได้โดยไม่ต้องลงอะไร

Node.js 26 ขึ้นไป เปิด Temporal เป็นค่าเริ่มต้นตั้งแต่ 26.0.0 (พฤษภาคม 2026) Node 24 ลงไปยังต้องใช้ polyfill

คำสั่งและค่าตั้งในบทความทดสอบบนระบบตัวอย่าง ลองบนเครื่องทดสอบก่อนใช้กับระบบจริงเสมอ

อ่านต่อ

#โค้ดเริ่มต้น

เก็บวันเวลาให้ถูก: เก็บเป็น UTC เก็บ timezone ผู้ใช้ แล้วค่อยแปลงตอนแสดง

เวลาเพี้ยน 7 ชั่วโมง วันเกิดเลื่อน นัดหมายเคลื่อนตอน DST แก้ด้วยกฎสามข้อ ใช้ timestamptz ใน Postgres, zoneinfo ใน Python, Temporal ใน JS และแปลงเป็น พ.ศ. ตอนแสดงเท่านั้น

อ่าน 13 นาที
#โค้ดกลาง

Go ใน Docker: GOMAXPROCS ตาม CPU limit (Go 1.25) และ Green Tea GC (Go 1.26)

Go 1.25 ตั้ง GOMAXPROCS ตาม CPU limit ของ container เอง Go 1.26 เปิด Green Tea GC เป็นค่าเริ่มต้น สิ่งที่ต้องเช็กใน Dockerfile และ go.mod พร้อมผลวัดจริง

อ่าน 9 นาที
#โค้ดกลาง

Python 3.15: lazy import, frozendict และ UTF-8 ค่าเริ่มต้น แก้ไฟล์ไทยบน Windows

Python 3.15 ออกแล้ว lazy import ทำให้ CLI เปิดเร็วขึ้นหลายเท่า frozendict ใช้เป็น key ได้ และ open() อ่านเขียน UTF-8 เสมอ ไฟล์ภาษาไทยบน Windows ไม่เพี้ยนอีก

อ่าน 12 นาที