🧰 ของที่ใช้ตรวจในบทนี้
tsc Version 7.0.2 # ได้จาก tsc -v
node v24.14.0 # ได้จาก node --version
เฉลยทุกข้อในบทนี้ผ่าน tsc --noEmit --strict จริง
แล้ว compile ด้วย tsc --target es2022 --module nodenext --moduleResolution nodenext
และรันด้วย node จริง
ทุกบรรทัดผลลัพธ์กับทุกข้อความ error ในบทนี้ก็อปมาจากหน้าจอจริง ไม่ได้เขียนจากความจำ
ข้อมูลตั้งต้นทุกชุดลอกมาจากสไลด์ตรง ๆ ไม่ได้แต่งเพิ่ม
วิธีใช้บทนี้ให้ได้ผลจริง
ลำดับที่ผมอยากให้ทำ ต่อโจทย์หนึ่งข้อ:
- อ่านโจทย์ + ดูข้อมูลตั้งต้น
- อ่านหัวข้อ "คำถามที่โจทย์ไม่ได้ตอบ" ก่อน แล้วตัดสินใจว่าคุณจะเลือกทางไหน — จดไว้
- เปิดไฟล์
.tsของตัวเองแล้วเขียน - รัน
tsc --noEmit --strict ไฟล์.tsให้ผ่านก่อน แล้วค่อยรันดูค่า - ค่อยกดปุ่ม เฉลย
โค้ดพวกนี้อ่านแล้วเข้าใจง่ายมาก จนสมองหลอกว่า "เออ เราก็เขียนได้"
ความรู้สึกนั้นเชื่อไม่ได้ — วิธีเดียวที่จะรู้ว่าเขียนได้จริงคือพิมพ์มันออกมาแล้วให้ tsc ตรวจ
1 · Grocery App — กรองของที่มีอยู่ แล้วทำชื่อเป็นตัวพิมพ์ใหญ่
โจทย์จากสไลด์หน้า 40: "Create function to filter available items and map names to UPPERCASE."
ข้อมูลตั้งต้นที่ให้มา:
interface GroceryItem {
name: string;
price: number;
available: boolean;
}
const groceries: GroceryItem[] = [
{ name: "apple", price: 15, available: true },
{ name: "milk", price: 45, available: false },
{ name: "bread", price: 35, available: true },
{ name: "butter", price: 60, available: false },
{ name: "eggs", price: 55, available: true },
];
ก่อนเขียน — จะใช้ตัวไหน แล้วเรียงยังไง?
โจทย์ใช้คำสองคำที่เป็นชื่อ method ตรง ๆ อยู่แล้ว: filter กับ map
คำถามที่เหลือคือ ลำดับ — filter ก่อนแล้ว map หรือกลับกัน
ทั้งสองลำดับให้ผลลัพธ์เหมือนกันในข้อนี้ แต่งานที่เครื่องทำไม่เท่ากัน:
กรองก่อนแปลว่าแปลงแค่ 3 ชิ้น · แปลงก่อนแปลว่าแปลง 5 ชิ้นแล้วทิ้ง 2
และถ้าเรียง map ก่อน คุณจะไม่มี available ให้กรองแล้ว ถ้า map คืนแค่ string
① ฟังก์ชันคืนอะไร — string[] (รายชื่อล้วน)
หรือ GroceryItem[] ที่ชื่อกลายเป็นตัวใหญ่ (ยังมี price ติดมาด้วย)?
คำว่า "map names" เอียงไปทาง string[] แต่ถ้าหน้าเว็บต้องโชว์ราคาด้วย อีกแบบถูกกว่า
② เรียงลำดับ — คงลำดับเดิม หรือเรียง ก-ฮ? โจทย์เงียบ = คงลำดับเดิม (ทั้ง filter และ map ไม่สลับลำดับให้)
③ UPPERCASE ของภาษาอะไร — toUpperCase() ใช้กฎกลาง
ส่วน toLocaleUpperCase() ใช้กฎของภาษาผู้ใช้ · ข้อมูลชุดนี้เป็นอังกฤษล้วนเลยไม่ต่าง
แต่วันที่มีชื่อสินค้าภาษาตุรกีเข้ามา สองตัวนี้ให้ผลคนละแบบ
🎯 ภารกิจข้อ 1
- สร้างไฟล์
q1.tsวางข้อมูลตั้งต้นข้างบนลงไป - เขียนฟังก์ชันที่คืน
string[]ของชื่อของที่available: trueเป็นตัวพิมพ์ใหญ่ - รัน
tsc --noEmit --strict q1.tsให้ผ่าน - เขียนบรรทัดพิสูจน์:
console.log(groceries[0])หลังเรียกฟังก์ชัน แล้วดูว่าgroceriesตัวเดิมเปลี่ยนไปไหม
function availableNames(items: GroceryItem[]): string[] {
return items
.filter((item) => item.available)
.map((item) => item.name.toUpperCase());
}
console.log(availableNames(groceries));
console.log(groceries[0]);
ผลรันจริง:
[ 'APPLE', 'BREAD', 'EGGS' ]
{ name: 'apple', price: 15, available: true }
บรรทัดที่สองคือบรรทัดพิสูจน์ — groceries[0] ยังเป็น 'apple' ตัวเล็กเหมือนเดิม
filter กับ map สร้าง array ใหม่เสมอ ไม่แตะของเดิม
จำข้อนี้ไว้ เพราะข้อ 2 จะเจอ method ที่ทำตรงข้าม
ลังผลไม้เข้าโรงงาน วิ่งผ่านสายพานสองช่วง:
· ช่วงคัด — ลูกที่ช้ำถูกปัดออกข้างทาง เหลือแต่ลูกดีวิ่งต่อ (นี่คือ filter)
· ช่วงติดสติกเกอร์ — ทุกลูกที่วิ่งผ่านโดนแปะสติกเกอร์ยี่ห้อ (นี่คือ map)
เอาช่วงคัดไว้ก่อนเสมอ ไม่งั้นคุณเสียสติกเกอร์ไปกับลูกที่จะถูกทิ้งอยู่ดี
ที่ไม่เหมือนกัน: สายพานจริงหยิบผลไม้ออกจากลังเดิม —
พอจบกระบวนการ ลังเดิมพร่องไปจริง ๆ
แต่ filter กับ map ไม่ได้แตะลังเดิมเลย
มันเหมือนถ่ายสำเนาผลไม้ที่ผ่านเกณฑ์ลงลังใหม่ ส่วนลังเดิมยังครบ 5 ลูกเท่าเดิม
ความต่างข้อนี้แหละที่ทำให้เขียนต่อกันเป็นสายยาว ๆ ได้โดยไม่ต้องกลัวว่าข้อมูลต้นทางจะพัง
2 · Movie App — เอาหนัง 3 เรื่องคะแนนดีที่สุด เรียงมากไปน้อย
โจทย์จากสไลด์หน้า 41: "Create a function to filter top 3 best rating movie and sort highest to lowest."
interface Movie {
title: string;
rating: number;
genre: string;
}
const movies: Movie[] = [
{ title: "Inception", rating: 8.8, genre: "sci-fi" },
{ title: "The Room", rating: 3.7, genre: "drama" },
{ title: "Interstellar", rating: 8.6, genre: "sci-fi" },
{ title: "Avengers", rating: 7.1, genre: "action" },
{ title: "Parasite", rating: 8.5, genre: "thriller" },
{ title: "John Wick", rating: 7.4, genre: "action" },
];
ก่อนเขียน — คำว่า "filter top 3" ในโจทย์ หมายถึง .filter() จริงหรือเปล่า?
ไม่ใช่ — และนี่คือจุดที่คนพลาดกันมากที่สุดในข้อนี้
.filter() ตัดสินทีละชิ้นด้วยเงื่อนไขที่ดูจากชิ้นนั้นชิ้นเดียว
แต่ "top 3" ต้องรู้จักทั้งกองก่อนถึงจะบอกได้ว่าใครติดสามอันดับแรก
ท่าที่ตรงกับโจทย์คือ เรียงก่อน แล้วตัดเอา 3 ตัวแรก — sort แล้ว slice
const best = movies.sort((a, b) => b.rating - a.rating).slice(0, 3);
console.log(best.map((m) => m.title));
console.log(movies.map((m) => m.title)); // ← บรรทัดนี้
บรรทัดสุดท้ายพิมพ์อะไรออกมา? (ยังไม่ได้แตะตัวแปร movies เลยสักที)
[ 'Inception', 'Interstellar', 'Parasite' ]
[
'Inception',
'Interstellar',
'Parasite',
'John Wick',
'Avengers',
'The Room'
]
movies ถูกเรียงใหม่ไปแล้ว ทั้งที่โค้ดไม่มีตรงไหนสั่งให้เปลี่ยนมันเลย
เพราะ Array.prototype.sort เรียงในตัว array เดิม แล้วคืน array เดิมกลับมา
(ไม่ใช่ตัวใหม่แบบ filter/map)
ในหน้าเว็บจริง แปลว่าตารางที่ผูกกับ movies อยู่จะสลับลำดับตามไปด้วย
ทั้งที่คุณแค่อยากได้ top 3 ไปโชว์อีกกล่องหนึ่ง
① คะแนนเท่ากันเอาใครก่อน — ถ้ามีหนัง 4 เรื่องได้ 8.5 เท่ากัน ใครติด top 3?
เรียงตามชื่อ? ตามวันฉาย? หรือ "เอาตามที่มันมาในกองอยู่แล้ว"?
ผมรันดูกับข้อมูล 4 เรื่องที่คะแนน 8.5 เท่ากันหมด ได้ [ 'A', 'B', 'C' ] —
คือคงลำดับเดิม เพราะ sort ของ JavaScript รับประกันความเสถียร (stable) มาตั้งแต่ ES2019
แต่ "ตามลำดับที่มันมาในกอง" ≠ "กติกาที่ธุรกิจต้องการ" — ยังต้องถามอยู่ดี
② มีหนังน้อยกว่า 3 เรื่องล่ะ — slice(0, 3) คืนเท่าที่มี ไม่ error
ต้องยืนยันว่านั่นคือสิ่งที่ต้องการ ไม่ใช่ต้อง error
③ เป็น 3 ตายตัวหรือส่งเลขเข้ามาได้ — ถ้าพรุ่งนี้ขอ top 5 ต้องแก้โค้ดข้างในไหม
🎯 ภารกิจข้อ 2
- เขียนฟังก์ชัน
topRated(list, n)คืนหนังnเรื่องคะแนนสูงสุด เรียงมากไปน้อย - ห้ามให้
moviesตัวเดิมเปลี่ยนลำดับ - รัน
tsc --noEmit --strictให้ผ่าน - เขียนบรรทัดพิสูจน์:
console.log(movies.map((m) => m.title))หลังเรียกฟังก์ชัน แล้วดูว่าลำดับยังเป็นลำดับเดิมของสไลด์อยู่ไหม
function topRated(list: Movie[], n: number): Movie[] {
return [...list] // ← ก็อปกองใหม่ก่อน
.sort((a, b) => b.rating - a.rating)
.slice(0, n);
}
console.log(topRated(movies, 3).map((m) => `${m.title} ${m.rating}`));
ผลรันจริง:
[ 'Inception 8.8', 'Interstellar 8.6', 'Parasite 8.5' ]
[...list] คือหัวใจของข้อนี้ — ก็อปกองใหม่ก่อน แล้วค่อยเรียง
ตัวเปรียบเทียบ b.rating - a.rating อ่านว่า "มากไปน้อย"
ถ้าเขียนกลับเป็น a.rating - b.rating จะได้น้อยไปมาก
มีอีกท่าที่สั้นกว่า: toSorted() ซึ่งคืน array ใหม่ให้เลยโดยไม่ต้องก็อปเอง
แต่มันมาใน ES2023 — บนเครื่องนี้ตั้ง --target es2022 แล้วใช้ toSorted จะได้ error จริงว่า:
error TS2550: Property 'toSorted' does not exist on type 'Movie[]'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2023' or later.
เปลี่ยนเป็น --target es2023 แล้วผ่านทันที · ถ้าโปรเจกต์ยังตั้ง target ต่ำอยู่ ให้ใช้ [...list].sort() ไปก่อน
เพื่อนขอให้คุณ "บอกหน่อยว่า 3 เล่มไหนบนชั้นนี้หนาที่สุด" มีสองวิธี:
· รื้อชั้นเขาเรียงใหม่จากหนาไปบาง แล้วชี้สามเล่มแรก — ได้คำตอบถูก
แต่ชั้นหนังสือของเพื่อนไม่เหมือนเดิมอีกแล้ว และเขาไม่ได้ขอ
· ถ่ายรูปชั้นไว้ แล้วไปเรียงในรูป — ชั้นจริงไม่ขยับ
movies.sort(...) คือวิธีแรก · [...movies].sort(...) คือวิธีที่สอง
ที่ไม่เหมือนกัน: เพื่อนเดินมาเห็นชั้นถูกรื้อก็รู้ทันทีว่ามีคนมายุ่ง
แต่โค้ดไม่มีใครมาทัก — sort ไม่ error ไม่ warn และ tsc --strict ก็ไม่ฟ้อง
เพราะในสายตาของ type system คุณแค่เรียก method ที่ถูกต้องบนตัวแปรที่ถูกต้อง
อาการจะโผล่ที่อีกหน้าจอหนึ่งที่ใช้ movies อยู่ แล้วคนก็จะไปไล่บั๊กผิดที่
- mutating method / in-place
- method ที่แก้ของเดิมแทนที่จะสร้างของใหม่ · ใน array มี
sort·reverse·push·splice· ถาม senior ได้ว่า “ฟังก์ชันนี้ mutate argument ที่รับเข้ามาไหม” คำถามนี้ตอบสั้นและกันบั๊กได้เยอะกว่าถามว่า “ฟังก์ชันนี้ทำอะไร” - immutable / non-mutating
- ไม่แตะของเดิม คืนของใหม่เสมอ ·
filter·map·slice·toSortedเป็นแบบนี้ · พิมพ์บอก AI ได้ว่า “rewrite this without mutating the input array” แล้วมันจะเปลี่ยนsortเป็น[...arr].sort()ให้เอง - stable sort
- เรียงแล้วของที่ "เท่ากัน" ยังคงลำดับเดิมที่มันมา · JavaScript รับประกันข้อนี้ตั้งแต่ ES2019 · ใช้คำนี้ตอนคุยเรื่องคะแนนเท่ากัน จะสื่อกว่า “มันเรียงมั่ว ๆ ไหม”
- comparator
- ฟังก์ชันที่ส่งให้
sortเพื่อบอกว่า "ใครมาก่อน" · คืนติดลบ =aมาก่อน · คืนบวก =bมาก่อน · คืน0= เสมอ · พูดว่า “comparator เรียงจากมากไปน้อย” จะตรงกว่า “ใส่ b ลบ a”
3 · Food Delivery — ยอดออเดอร์ที่ส่งแล้ว เกิน 500 คืนเงิน 10%
โจทย์จากสไลด์หน้า 42: "Create a function to calculate delivered orders if total more than 500 apply 10% to cash back to customer."
interface Order {
id: number;
item: string;
price: number;
status: "delivered" | "pending" | "cancelled";
}
const orders: Order[] = [
{ id: 1, item: "Pizza", price: 280, status: "delivered" },
{ id: 2, item: "Sushi", price: 450, status: "cancelled" },
{ id: 3, item: "Burger", price: 150, status: "delivered" },
{ id: 4, item: "Pasta", price: 220, status: "delivered" },
{ id: 5, item: "Salad", price: 120, status: "pending" },
];
ก่อนเขียน — จะใช้ตัวไหน?
ข้อนี้ต้องใช้ สามตัวต่อกัน: filter เอาเฉพาะ "delivered" →
reduce รวมราคาให้เหลือตัวเลขเดียว → แล้วค่อยตัดสินใจเรื่องส่วนลด
ตัวที่ยังไม่เคยใช้ในสองข้อแรกคือ reduce — มันคือตัวที่ยุบ array ให้เหลือค่าเดียว
reduce ที่ลืมใส่ค่าตั้งต้น — tsc จับได้ แต่ด้วยข้อความที่อ่านไม่รู้เรื่อง
เขียนแบบนี้ (ลืมใส่ , 0 ท้ายสุด):
.reduce((sum, o) => sum + o.price)
ได้ error จริงว่า:
error TS2365: Operator '+' cannot be applied to types 'Order' and 'number'.
อ่านแล้วงงว่าทำไม sum ถึงกลายเป็น Order — เพราะถ้าไม่ใส่ค่าตั้งต้น
reduce จะหยิบสมาชิกตัวแรกมาเป็นค่าตั้งต้น ซึ่งเป็น Order ไม่ใช่ number
และถ้ารันบน array ว่างในสภาพนั้น จะได้ของแรงกว่านั้น — รันจริงบน node ได้
TypeError: Reduce of empty array with no initial value
กฎง่าย ๆ: ใส่ค่าตั้งต้นให้ reduce เสมอ แม้จะรู้สึกว่าไม่จำเป็น
① "more than 500" คือ > หรือ >= —
ยอด 500 บาทพอดี ได้คืนเงินไหม? ภาษาไทยว่า "เกิน 500" กับ "ตั้งแต่ 500" คนละอย่าง
และลูกค้าที่จ่าย 500 พอดีจะเป็นคนโทรมาถาม
② คิด 10% จากยอดรวม หรือจากรายออเดอร์ —
ยอดรวม 650 คืน 65 · แต่ถ้าคิดรายออเดอร์ ไม่มีออเดอร์ไหนถึง 500 เลยสักอัน คืน 0
ต่างกันคนละโลก จากประโยคเดียวกัน
③ ฟังก์ชันคืนอะไร — ยอดหลังหักแล้ว (585) หรือคืนทั้งสามตัวเลข (ยอดรวม · เงินคืน · ยอดจ่ายจริง)?
ถ้าคืนแค่ 585 ตัวเดียว หน้าเว็บจะเอาอะไรไปโชว์ว่า "คุณได้คืน 65 บาท"
④ "pending" นับไหม — โจทย์เขียนว่า delivered orders ก็ควรแปลว่าไม่นับ
แต่ในธุรกิจจริง "ยอดที่จะได้คืน" กับ "ยอดที่ส่งแล้ว" อาจเป็นคนละตัว
⑤ ปัดเศษยังไง — ดูหัวข้อถัดไป
🎯 ภารกิจข้อ 3
- เขียนฟังก์ชันที่รับ
Order[]แล้วคืน object ที่มีทั้งสามตัวเลข: ยอดรวมของที่ส่งแล้ว · เงินคืน · ยอดจ่ายจริง - ประกาศ
interfaceของค่าที่คืนด้วย อย่าปล่อยให้tscเดาเอง - เลือกเองว่า
>หรือ>=แล้วเขียนคอมเมนต์กำกับว่าเลือกอันไหน - เขียนบรรทัดพิสูจน์: ยิงข้อมูลอีกชุดที่ยอดรวม 500 พอดี เข้าไป แล้วดูว่าผลตรงกับที่คุณตั้งใจไหม
interface Bill {
total: number;
cashBack: number;
payable: number;
}
function summarize(list: Order[]): Bill {
const total = list
.filter((o) => o.status === "delivered")
.reduce((sum, o) => sum + o.price, 0); // ← ค่าตั้งต้น 0
// เลือกแล้วว่า "เกิน 500" = มากกว่า 500 (500 พอดีไม่ได้คืน)
const cashBack = total > 500 ? total * 0.1 : 0;
return { total, cashBack, payable: total - cashBack };
}
console.log(summarize(orders));
ผลรันจริง:
{ total: 650, cashBack: 65, payable: 585 }
ยอด 650 มาจาก Pizza 280 + Burger 150 + Pasta 220 — Sushi ถูกยกเลิก Salad ยังไม่ส่ง
บรรทัดพิสูจน์ที่ยิงข้อมูลยอดรวม 500 พอดี เข้าไป ได้:
{ total: 500, cashBack: 0, payable: 500 }
ซึ่งตรงกับที่เลือกไว้ในคอมเมนต์ · ถ้าคนออกโจทย์ตั้งใจให้ 500 ได้คืนด้วย
แก้ที่เดียวคือเปลี่ยน > เป็น >= — แต่ต้องรู้ว่ากำลังเปลี่ยนอะไร
เรื่องปัดเศษ — ผมรันแล้วผลไม่ตรงกับที่คาดไว้ ขอเล่าตามจริง
ก่อนรัน ผมคิดว่าจะได้โชว์กับดักเลขทศนิยมของคอมพิวเตอร์ตรงนี้พอดี
แต่รันจริงแล้ว 650 * 0.1 ได้ 65 เป๊ะ ๆ ไม่มีเศษหลุด
> 650 * 0.1
65
ข้อมูลชุดนี้บังเอิญปลอดภัย ไม่ได้แปลว่าปัญหาไม่มี
ผมเลยลองยิงราคาที่มีทศนิยมแบบที่ร้านอาหารใช้จริง แล้วมันโผล่ทันที:
> 129.9 * 3
389.70000000000005
> 0.1 + 0.2
0.30000000000000004
> 129.9 + 220.1 + 300.5
650.5
> 650.5 * 0.1
65.05
389.70000000000005 บาท ไม่มีอยู่จริงในโลก
ตัวเลขทศนิยมของคอมพิวเตอร์เก็บด้วยฐานสอง ซึ่งเก็บ 0.1 แบบเป๊ะ ๆ ไม่ได้
เหมือนที่เลขฐานสิบเขียน 1/3 แบบเป๊ะ ๆ ไม่ได้ (0.333… ไม่มีวันจบ)
บางคู่ตัวเลขบวกลบคูณแล้วลงตัวพอดี (อย่าง 650 * 0.1) บางคู่ไม่ลงตัว
และคุณเดาล่วงหน้าไม่ได้ว่าคู่ไหนเป็นคู่ไหน
ทางแก้ที่ใช้กันจริง มีสองท่า:
· ปัดตอนแสดงผล — Math.round(x * 100) / 100 หรือ x.toFixed(2)
· เก็บเป็นสตางค์ (จำนวนเต็ม) ตั้งแต่ต้น — 129.90 บาท เก็บเป็น 12990
แล้วหารร้อยตอนโชว์ · ระบบเงินจริงใช้ท่านี้
ผมรัน Math.round(650 * 0.1 * 100) / 100 บนข้อมูลชุดสไลด์ ได้ 65 เท่าเดิม —
ปัดเศษกับข้อมูลชุดนี้ไม่ทำให้ผลเปลี่ยน แต่มันกันวันที่ข้อมูลเปลี่ยน
- accumulator (ค่าสะสมใน
reduce) - ตัวแปรที่วิ่งสะสมค่าไปเรื่อย ๆ ระหว่างเดินผ่านทุกสมาชิก — ในเฉลยคือ
sum· ตัวที่สองของreduceคือ ค่าตั้งต้นของ accumulator · ถาม senior ได้ว่า “accumulator ของ reduce ตัวนี้เป็น type อะไร” ถ้าตอบไม่ได้ทันที แปลว่าควรใส่ type ให้มันชัด ๆ - edge case / boundary condition
- ค่าที่อยู่ตรงเส้นแบ่งพอดี — ในข้อนี้คือยอด
500เป๊ะ · ถามคนออกโจทย์ตรง ๆ ว่า “ยอด 500 พอดี ได้คืนเงินไหม” เป็นคำถามที่ดูจุกจิกแต่ประหยัดเวลาแก้งานได้ครึ่งวัน - floating point
- วิธีเก็บทศนิยมของคอมพิวเตอร์ ที่ปัดเศษเล็ก ๆ ทิ้งเสมอ ·
พิมพ์บอก AI ได้ว่า “this is money — use integer cents instead of floating point”
แล้วมันจะเปลี่ยนโครงข้อมูลให้ ไม่ใช่แค่เติม
toFixed
4 · HR Payroll — รวมเงินเดือนพนักงาน full-time แยกตามตำแหน่ง
โจทย์จากสไลด์หน้า 43: "Create a function to calculate the total monthly salary for full-time by roles."
interface Employee {
name: string;
role: string;
type: "full-time" | "part-time";
dailyRate: number;
workDays: number;
}
const employees: Employee[] = [
{ name: "Alice", role: "developer", type: "full-time", dailyRate: 2000, workDays: 22 },
{ name: "Bob", role: "designer", type: "full-time", dailyRate: 1800, workDays: 22 },
{ name: "Carol", role: "developer", type: "part-time", dailyRate: 1200, workDays: 10 },
{ name: "Dave", role: "developer", type: "full-time", dailyRate: 2200, workDays: 22 },
{ name: "Eve", role: "manager", type: "full-time", dailyRate: 2500, workDays: 22 },
];
ก่อนเขียน — ข้อนี้ต่างจากข้อ 3 ตรงไหน?
ข้อ 3 ยุบทั้งกองให้เหลือตัวเลขเดียว · ข้อนี้ยุบให้เหลือobject ที่มีหลายช่อง
(developer เท่าไหร่ · designer เท่าไหร่ · manager เท่าไหร่)
แต่ตัวที่ใช้ยังเป็น reduce ตัวเดิม — เปลี่ยนแค่ชนิดของ accumulator
จาก number เป็น Record<string, number>
นี่คือจุดที่คนเข้าใจ reduce จริง ๆ กับคนที่จำสูตร sum ได้อย่างเดียวจะแยกกันชัด
① "monthly" คำนวณจากอะไร — dailyRate * workDays ก็เป็นการเดาอย่างหนึ่ง
(แต่เป็นการเดาที่สมเหตุสมผล เพราะโจทย์ให้มาแค่สองช่องนี้)
② role ที่ไม่มีพนักงาน full-time เลย ต้องมีในผลลัพธ์ไหม —
ถ้าบริษัทมีตำแหน่ง intern ที่เป็น part-time หมด ผลลัพธ์ควรมี intern: 0
หรือไม่ควรมี key นั้นเลย? หน้าเว็บที่วนแสดงผลจะได้ผลต่างกัน
③ คืนเป็นอะไร — object ธรรมดา · Map · หรือ array ของ
{ role, total } ที่เรียงได้? ถ้าต้องเอาไปเรียงจากมากไปน้อยต่อ array สะดวกกว่า
④ role เป็น string เฉย ๆ —
แปลว่า "Developer" กับ "developer" จะกลายเป็นคนละตำแหน่งทันที
และ tsc ไม่มีทางเตือน เพราะทั้งคู่เป็น string ที่ถูกต้องทั้งคู่
🎯 ภารกิจข้อ 4
- เขียนฟังก์ชันที่คืน
Record<string, number>— key คือ role, value คือยอดรวมต่อเดือนของ full-time - ใช้
reduceโดยใส่ type ให้ accumulator ชัด ๆ (ท่าที่อ่านง่ายสุดคือ.reduce<Record<string, number>>(...)) - รัน
tsc --noEmit --strictให้ผ่าน - เขียนบรรทัดพิสูจน์:
console.log(Object.keys(result))แล้วดูว่ามี key กี่ตัว · แล้วลองอ่าน key ที่ไม่มีอยู่จริงดูว่าได้อะไร
function payrollByRole(list: Employee[]): Record<string, number> {
return list
.filter((e) => e.type === "full-time")
.reduce<Record<string, number>>((acc, e) => {
acc[e.role] = (acc[e.role] ?? 0) + e.dailyRate * e.workDays;
return acc;
}, {});
}
const result = payrollByRole(employees);
console.log(result);
console.log(result["intern"]);
console.log(Object.keys(result));
ผลรันจริง:
{ developer: 92400, designer: 39600, manager: 55000 }
undefined
[ 'developer', 'designer', 'manager' ]
ตรวจเลขด้วยมือ: developer = Alice 2000×22 + Dave 2200×22 = 44000 + 48400 = 92400 · Carol เป็น part-time เลยไม่ถูกนับ (ถ้านับ จะได้ 104400)
บรรทัด result["intern"] คือบรรทัดพิสูจน์ — ได้ undefined
ทั้งที่ tsc บอกว่ามันเป็น number · หัวข้อถัดไปอธิบายว่าทำไม
--strict เพียว ๆ จับไม่ได้ — ลืม ?? 0 แล้วได้ NaN
ลบ ?? 0 ออกจากเฉลยข้างบน เหลือแค่นี้:
acc[e.role] = acc[e.role] + e.dailyRate * e.workDays;
tsc --noEmit --strict ผ่านฉลุย ไม่ฟ้องอะไรเลย · แต่รันจริงได้:
{ developer: NaN, designer: NaN, manager: NaN }
เพราะรอบแรกของแต่ละ role acc[e.role] ยังไม่มีค่า → ได้ undefined
→ undefined + 44000 = NaN → แล้ว NaN บวกอะไรต่อก็เป็น NaN ตลอดสาย
ทำไม --strict ไม่จับ: Record<string, number> แปลว่า
"key อะไรก็ได้ที่เป็น string ชี้ไปหา number" — ในสายตา tsc
acc["ตำแหน่งที่ไม่มีจริง"] ก็ยังเป็น number
มีตัวจับได้จริง แต่ต้องเปิดเพิ่ม: ใส่ flag --noUncheckedIndexedAccess
แล้ว tsc ฟ้องทันทีว่า
error TS2532: Object is possibly 'undefined'.
ผมรันเทียบทั้งสองแบบบนไฟล์เดียวกัน — --strict เฉย ๆ ได้ exit=0
ส่วน --strict --noUncheckedIndexedAccess ได้ exit=1 พร้อม error ข้างบน
ถ้าโปรเจกต์ของคุณอ่านค่าจาก object ด้วย key ที่มาจากข้อมูล ให้เปิด flag นี้
ฝ่ายบุคคลถือกล่องซองเปล่าเดินไปตามโต๊ะพนักงานทีละคน
เจอ Alice (developer) → หาซองชื่อ "developer" ในกล่อง · ยังไม่มี ก็เขียนซองใหม่
แล้วหยอด 44,000
เจอ Dave (developer) → ซอง "developer" มีแล้ว หยอดเพิ่ม 48,400 เข้าซองเดิม
เจอ Carol (part-time) → เดินผ่านไปเฉย ๆ
กล่องซองคือ accumulator · การ "เขียนซองใหม่ถ้ายังไม่มี" คือ ?? 0 นั่นเอง
ที่ไม่เหมือนกัน: คนจริงที่ล้วงเข้าไปในกล่องแล้วไม่เจอซอง จะรู้ตัวทันที
แล้วหยิบซองใหม่มาเขียนเอง
แต่ JavaScript ล้วงไม่เจอแล้วยื่นความว่างเปล่า (undefined) กลับมาให้เฉย ๆ
เหมือนไม่มีอะไรผิด · พอเอาไปบวกเลข ยอดทั้งซองกลายเป็น NaN
แล้ว NaN ก็ติดเชื้อไปทุกซองที่คำนวณต่อจากมัน
นี่คือเหตุผลที่ต้องเขียนบรรทัดพิสูจน์ที่อ่าน key ที่ไม่มีจริง ตั้งแต่ตอนเขียนโค้ด
Record<string, number>- type ที่แปลว่า "object ที่ key เป็น
stringอะไรก็ได้ และ value เป็นnumber" · เรียกอีกชื่อว่า index signature · พูดว่า “คืนเป็น Record<string, number> นะ” จะตรงกว่า “คืนเป็น object” ??(nullish coalescing)- "ถ้าฝั่งซ้ายเป็น
nullหรือundefinedให้ใช้ฝั่งขวาแทน" · ต่างจาก||ตรงที่0กับ""ผ่าน??ไปได้ แต่โดน||เปลี่ยนเป็นค่าฝั่งขวา — เรื่องเงินใช้??เท่านั้น NaN- "Not a Number" — ผลของการคำนวณเลขที่ทำไม่ได้ · มันติดเชื้อ:
อะไรบวกกับมันก็เป็น
NaN· พิมพ์ถาม AI ได้ว่า “where does this NaN come from — trace the first undefined” เพราะจุดที่เห็นNaNมักไม่ใช่จุดที่มันเกิด noUncheckedIndexedAccess- flag ของ TypeScript ที่ทำให้การอ่านค่าจาก index/key คืน
T | undefinedแทนT· ถาม senior ได้ว่า “โปรเจกต์เราเปิด noUncheckedIndexedAccess ไหม” คำตอบบอกได้เลยว่าคุณต้องเช็คundefinedเองแค่ไหน
5 · สรุปคำถามที่ต้องถามก่อนพิมพ์บรรทัดแรก
ทั้งสี่ข้อมีรูปแบบเดียวกัน: โจทย์บอกว่าจะทำอะไร แต่ไม่ได้บอกว่าขอบมันอยู่ตรงไหน
ตารางนี้คือของที่ผมจะพิมพ์ส่งกลับไปถามคนออกโจทย์จริง ๆ ก่อนลงมือ:
| ข้อ | คำถามที่ต้องถาม | ถ้าเดาผิดจะเกิดอะไร |
|---|---|---|
| 1 · Grocery | คืน string[] หรือ GroceryItem[] |
หน้าเว็บโชว์ราคาไม่ได้ ต้องกลับมาแก้ signature ทั้งสาย |
| 2 · Movie | คะแนนเท่ากันเอาใครก่อน · หนังน้อยกว่า 3 เรื่องทำยังไง | ผลลัพธ์ถูกบ้างผิดบ้างขึ้นกับข้อมูล — บั๊กที่เทสจับไม่ได้ถ้าไม่ได้ตั้งใจใส่ค่าเสมอ |
| 3 · Food | "เกิน 500" คือ > หรือ >= · คิด 10% จากยอดรวมหรือรายออเดอร์ |
คำนวณเงินผิด · ลูกค้าที่จ่าย 500 พอดีจะเป็นคนโทรมาถาม |
| 4 · HR | role ที่ไม่มี full-time ต้องมี key ไหม · คืน object หรือ array | หน้าเว็บวนลูปแล้วตำแหน่งบางอันหายไปเงียบ ๆ |
"ขอเช็คก่อนเขียนนะครับ — ยอด 500 พอดี ถือว่าได้คืนเงินไหม
และ 10% คิดจากยอดรวมทั้งบิล หรือคิดทีละออเดอร์ครับ"
คำถามแบบนี้ใช้เวลาถาม 20 วินาที และประหยัดเวลาแก้งานได้เป็นชั่วโมง
คนที่ถามคำถามแบบนี้ก่อนเขียน คือคนที่ดูเก่งขึ้นในสายตาคนอื่น ไม่ใช่ตรงกันข้าม —
เพราะมันแปลว่าคุณเห็นเคสที่คนอื่นยังไม่เห็น
6 · Recap ของทั้งกลุ่ม (สไลด์หน้า 44–48)
สไลด์ปิดท้ายด้วยการทวนสี่เรื่องที่ใช้ในโจทย์ทุกข้อ:
Core Types — ชนิดพื้นฐานและกอง (หน้า 45)
/** ===== PRIMITIVE TYPE ===== */
const message: string = "Hello World";
const numeric: number = 1000;
const isEnabled: boolean = true;
/** ===== ARRAY COLLECTION ===== */
const scoreList: Array<number> = [40, 45, 50, 43, 50];
const students: Array<string> = [
"John Doe",
"Frederic Castro",
"Josie Watson",
"Claude Cole",
"Dominik James",
];
Array<number> กับ number[] เป็นอันเดียวกันเป๊ะ เขียนได้ทั้งสองแบบ
สไลด์ใช้ Array<T> ในหน้านี้ แต่ในหน้าโจทย์ทั้งสี่ข้อใช้ Movie[] · Order[] —
แบบหลังพบบ่อยกว่าในโค้ดจริง
Interface และ Type — สองวิธีตั้งชื่อรูปร่างข้อมูล (หน้า 46)
type PairTuple = [number, boolean, string];
type StudentIdType = number | string;
type ClassroomType = {
name: string;
capability: number;
};
interface IGameSession {
round: string;
score: number;
}
interface ITwentyFourGame extends IGameSession {
streak: number;
hasTimer: boolean;
}
| อยากได้อะไร | ใช้ตัวไหน | โจทย์ข้อไหนใช้ |
|---|---|---|
| รูปร่างของ object | ได้ทั้ง interface และ type | ทั้ง 4 ข้อ (สไลด์เลือก interface) |
| รายการค่าที่เลือกได้ (union) | ต้องใช้ type — interface ทำไม่ได้ | ข้อ 3 (status) · ข้อ 4 (type) |
| tuple (กองที่รู้จำนวนและชนิดของทุกช่อง) | ต้องใช้ type | ไม่มีข้อไหนใช้ |
| ต่อยอดจากรูปร่างเดิม | interface … extends … | ไม่มีข้อไหนใช้ |
status: "delivered" | "pending" | "cancelled" คือของที่ทำให้ข้อ 3 ปลอดภัย
ถ้าประกาศเป็น status: string เฉย ๆ แล้วพิมพ์ผิดเป็น "deliverd"
โค้ดจะรันได้ปกติ แล้วคืนยอด 0 เงียบ ๆ
แต่เพราะสไลด์ประกาศเป็น union ของสามค่า ผมลองพิมพ์ผิดดูแล้วได้ error จริงทันที:
error TS2367: This comparison appears to be unintentional because the types '"cancelled" | "delivered" | "pending"' and '"deliverd"' have no overlap.
นี่คือค่าตัวจริงของการเขียน union แทน string —
ย้อนกลับไปดูข้อ 4 ที่ role เป็น string เฉย ๆ จะเห็นความต่างชัดเลย
Function — เขียนได้สองแบบ (หน้า 47)
function sum(num1: number, num2: number): number {
return num1 + num2
}
const sum = (num1: number, num2: number): number => {
return num1 + num2
}
ทั้งสองแบบใช้ได้ · แบบล่าง (arrow function) คือแบบที่คุณเขียนไปแล้วเป็นสิบครั้งในบทนี้
ตอนส่งเข้า filter · map · reduce — แค่ไม่ได้ตั้งชื่อให้มัน
ทั้งคู่ตั้งชื่อว่า sum เหมือนกัน · ผมลองวางทั้งสองก้อนในไฟล์เดียวแล้วรัน
tsc --noEmit --strict ได้ error จริงสองบรรทัด:
error TS2300: Duplicate identifier 'sum'.
(ฟ้องทั้งบรรทัดที่ 1 และบรรทัดที่ 5 — คือฟ้องทั้งสองก้อนพร้อมกัน
เพราะ tsc ไม่รู้ว่าคุณตั้งใจให้ตัวไหนเป็นตัวจริง)
เอาไปทีละก้อน หรือเปลี่ยนชื่อก้อนหนึ่งเป็น sumArrow
ถ้าอยากลองเทียบสองแบบในไฟล์เดียว
Transform Element — เปลี่ยนรูปข้อมูล (หน้า 48)
| ตัว | ทำอะไร | แตะของเดิมไหม |
|---|---|---|
join(", ") | array → string เชื่อมด้วยตัวคั่น | ไม่แตะ |
split("") | string → array | ไม่แตะ |
slice(2, 6) | ตัดช่วงออกมา (เอา index 2 ถึงก่อน 6) | ไม่แตะ |
map(fn) | แปลงทุกชิ้นเป็นของใหม่ | ไม่แตะ |
filter(fn) | คัดเฉพาะที่ผ่านเงื่อนไข | ไม่แตะ |
reduce(fn, init) | ยุบทั้งกองเหลือค่าเดียว | ไม่แตะ |
sort(cmp) | เรียงลำดับ | ⚠️ แตะ! เรียงของเดิม |
slice กับ splice ชื่อต่างกันตัวเดียว แต่คนละเรื่องกันสิ้นเชิง
สไลด์หน้า 48 พาดหัวช่วงหนึ่งว่า SPLICE แต่โค้ดข้างล่างเรียก .slice(2, 6) จริง ๆ
— หัวข้อกับโค้ดไม่ตรงกัน ให้ยึดโค้ด
· slice = ตัดสำเนาช่วงหนึ่งออกมา ของเดิมครบเหมือนเดิม
· splice = ควักออกจากของเดิมจริง ๆ (และแทรกของใหม่เข้าไปได้ด้วย)
ทั้งสองตัวคืนค่าหน้าตาเหมือนกันเป๊ะ ความต่างอยู่ที่ของเดิม — ผมรันเทียบให้ดู:
// มี [...] นำหน้า (แบบเฉลยข้อ 2) — splice ควักแค่กองสำเนา ของเดิมไม่โดน
[...movies].sort((a, b) => b.rating - a.rating).splice(0, 3)
// คืน [ 'Inception', 'Interstellar', 'Parasite' ]
// movies เหลือ 6 เรื่อง ลำดับเดิมครบ ← ปลอดภัย
// ไม่มี [...] นำหน้า — splice ควักของจริง
movies.sort((a, b) => b.rating - a.rating).splice(0, 3)
// คืน [ 'Inception', 'Interstellar', 'Parasite' ] ← เหมือนกันเป๊ะ
// movies เหลือ [ 'John Wick', 'Avengers', 'The Room' ] ← หนังดีสามเรื่องหายไปจากตัวแปร!
สังเกตว่าบรรทัดที่พิมพ์ออกมาเหมือนกันทุกตัวอักษร ทั้งที่ตัวแปรต้นทางคนละชะตากรรม
· และไม่มี error ใด ๆ ทั้งตอน tsc และตอนรัน
นี่คือเหตุผลที่เฉลยข้อ 2 ต้องมี [...list] — มันคือตัวกันไม่ให้ความพลาดแบบนี้ลามไปถึงข้อมูลจริง
ทดสอบความเข้าใจ
1. เขียน const best = movies.sort((a, b) => b.rating - a.rating).slice(0, 3); แล้วรัน — เกิดอะไรขึ้นกับตัวแปร movies?
ผลรันจริงของบรรทัดถัดไป console.log(movies.map((m) => m.title)):
[ 'Inception', 'Interstellar', 'Parasite', 'John Wick', 'Avengers', 'The Room' ]
คือลำดับที่เรียงจากคะแนนมากไปน้อยแล้ว ไม่ใช่ลำดับเดิมของสไลด์
ตัวเลือกที่ 1 ผิดตรงที่ไปโทษ slice — slice ไม่แตะของเดิมจริง
แต่ความเสียหายเกิดที่ sort ซึ่งทำงานก่อน slice
ตัวเลือกที่ 2 เข้าใจ const ผิด — const ห้ามแค่การชี้ไปกองใหม่
ไม่ได้ห้ามสลับของข้างในกองเดิม · tsc --strict จึงเงียบสนิท
ตัวเลือกที่ 4 สับสน slice กับ splice — ตัวที่ควักของออกจริงคือ splice
จุดที่ต้องจำ: ก่อนเรียก sort บน array ที่ไม่ใช่ของตัวเอง ให้ก็อปก่อนเสมอ —
[...arr].sort(...)
2. โจทย์ข้อ 3 เขียนว่า "if total more than 500 apply 10% cash back" — ทำไมต้องถามคนออกโจทย์ก่อนเขียน?
สองจุดที่กำกวมคือจุดที่เปลี่ยนตัวเลขเงินที่ลูกค้าได้จริง:
ยอด 500 พอดีได้คืนไหม · และคิด 10% จากยอดรวม (650 → คืน 65)
หรือคิดทีละออเดอร์ (ไม่มีออเดอร์ไหนถึง 500 → คืน 0)
ตัวเลือกที่ 1 ผิดชัด — total * 0.1 คำนวณได้ตรง ๆ ไม่ต้องใช้ library
(เรื่องปัดเศษเป็นคนละประเด็นและแก้ได้เอง)
ตัวเลือกที่ 3 สลับที่มาของคำตอบ — ค่าตั้งต้นของ reduce คือ 0
ซึ่งคนเขียนโค้ดรู้เอง ไม่ต้องถามใคร
ตัวเลือกที่ 4 หยิบของจริงมาแต่ไม่ใช่จุดที่กำกวม — union สามค่าเป็นสิ่งที่โจทย์บอกไว้ชัดแล้ว
จุดที่ต้องจำ: คำถามที่ควรถามคือคำถามที่คำตอบเปลี่ยนผลลัพธ์
ไม่ใช่คำถามที่คุณหาคำตอบเองได้จากเอกสาร
3. ในเฉลยข้อ 4 ถ้าลบ ?? 0 ออก เหลือ acc[e.role] = acc[e.role] + e.dailyRate * e.workDays; — tsc --noEmit --strict จะว่ายังไง และรันแล้วได้อะไร?
ผมรันเทียบจริงบนไฟล์เดียวกัน: tsc --noEmit --strict ได้ exit=0 ไม่มี error
แล้ว node พิมพ์ { developer: NaN, designer: NaN, manager: NaN } ออกมา
เพราะ Record<string, number> สัญญาไว้ว่า key ไหนก็ได้จะให้ number —
tsc จึงไม่มีเหตุให้สงสัยว่าค่าจะหายไป · พอรันจริง key ที่ยังไม่มีคืน undefined
→ undefined + 44000 = NaN
ตัวเลือกที่ 1 ถูกเฉพาะเมื่อเปิด flag --noUncheckedIndexedAccess เพิ่ม
ซึ่งคำถามนี้ไม่ได้เปิด — ผมรันแบบเปิด flag แล้วได้ TS2532 จริงตามนั้น
ตัวเลือกที่ 2 หยิบเลข error ของอีกเคสหนึ่งมา (TS2365 คือเคส reduce ที่ลืมค่าตั้งต้นในข้อ 3)
ตัวเลือกที่ 3 คือความเชื่อที่อันตรายที่สุด — JavaScript ไม่ได้ ถือว่าค่าที่ยังไม่มีเท่ากับ 0
จุดที่ต้องจำ: --strict ไม่ได้แปลว่าปลอดภัยทุกเรื่อง —
การอ่านค่าด้วย key ที่มาจากข้อมูล เป็นช่องที่ต้องเปิด flag เพิ่มหรือเช็คเอง
4. โจทย์ข้อ 2 ใช้คำว่า "filter top 3 best rating movie" — ควรใช้ .filter() ไหม?
callback ของ .filter() เห็นแค่สมาชิกทีละตัว จึงตอบได้แค่คำถามแบบ "ตัวนี้ผ่านเกณฑ์ไหม"
แต่ "ติด 3 อันดับแรกไหม" เป็นคำถามที่ต้องเทียบกับทั้งกอง
ตัวเลือกที่ 2 เป็นกับดักที่ดีมาก เพราะ >= 8.5 ให้ผลถูกพอดีกับข้อมูลชุดนี้ (8.8 · 8.6 · 8.5)
— แต่มันคือการฮาร์ดโค้ดตัวเลขที่เดามาจากข้อมูลตัวอย่าง
เพิ่มหนังคะแนน 9.0 เข้าไปหนึ่งเรื่อง คุณจะได้ 4 เรื่องทันที
ตัวเลือกที่ 3 อ่านโจทย์เป็นคำสั่งเรียก method ซึ่งไม่ใช่ — "filter" ในภาษาคนแปลว่า "คัดเอา"
ตัวเลือกที่ 4 เป็นข้อมูลผิด — .filter() คงลำดับเดิมไว้เสมอ ไม่เรียงอะไรให้
จุดที่ต้องจำ: เจอโจทย์ที่มีคำว่า "top N" · "อันดับ" · "มากที่สุด"
ให้คิดถึง sort + slice ก่อน และอย่าเอาเงื่อนไขที่เดาจากข้อมูลตัวอย่างมาฮาร์ดโค้ด
ถามกลับได้เลย
ถ้าเฉลยข้อไหนอ่านแล้วยังไม่เห็นภาพ หรือคุณเขียนอีกท่าแล้วผลไม่ตรงกับในบท —
เอาโค้ดที่คุณเขียนมาถามได้เลย พร้อมบรรทัดที่ tsc ฟ้อง
โดยเฉพาะถ้าคุณเลือกตอบข้อกำกวมต่างจากในบท (เช่นเลือก >= แทน >) —
นั่นไม่ใช่ความผิด แต่เป็นจุดที่ควรเอามาคุยกันว่าทำไมถึงเลือกแบบนั้น
อ้างอิง
- MDN —
Array.prototype.sort()— อ่านย่อหน้าแรกกับหัวข้อ Description ตรงที่เขียนว่ามันเรียง in place และตรงที่ยืนยันว่า sort เป็น stable · เป็นที่มาของกับดักใหญ่ที่สุดในบทนี้ - MDN —
Array.prototype.reduce()— อ่านหัวข้อ initialValue ให้ครบ จะเห็นเลยว่าถ้าไม่ใส่แล้วจะเกิดอะไร ทั้งเรื่อง type และเรื่องTypeErrorบน array ว่าง - MDN —
Array.prototype.toSorted()— ดูตาราง browser compatibility ท้ายหน้า ก่อนตัดสินใจใช้แทน[...arr].sort()ในโปรเจกต์จริง - TypeScript —
noUncheckedIndexedAccess— อ่านตัวอย่างสองก้อนในหน้านั้น จะเห็นความต่างของTกับT | undefinedซึ่งคือเหตุผลที่ข้อ 4 ได้NaNโดยไม่มีใครเตือน - TypeScript Handbook — Everyday Types —
อ่านหัวข้อ Union Types และ Type Aliases vs Interfaces
เพื่อเข้าใจว่าทำไม
statusถึงเขียนเป็น union ได้แต่ต้องใช้typeไม่ใช่interface