Temporal ใน JavaScript ใช้งานจริง: บวกเดือน คำนวณอายุ และ polyfill สำหรับ Safari
Temporal เป็น Stage 4 ใช้ได้ใน Chrome 144, Firefox 139 และ Node.js 26 แล้ว โค้ดที่ใช้บ่อยแทน Date ต่อกับโค้ดเดิม แสดง พ.ศ. และโหลด polyfill เฉพาะ Safari
- ณ ตุลาคม 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
| ที่รัน | รองรับตั้งแต่ | หมายเหตุ |
|---|---|---|
| Firefox | 139 | browser แรกที่ส่ง Temporal |
| Chrome / Edge | 144 | ออกมกราคม 2026 |
| Node.js | 26.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:00 | Temporal.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
นับวันและคำนวณอายุ: 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
คำสั่งและค่าตั้งในบทความทดสอบบนระบบตัวอย่าง ลองบนเครื่องทดสอบก่อนใช้กับระบบจริงเสมอ