บทเรียน 68 · Frontend · Functional Programming with TypeScript

โจทย์ 4 ข้อจากสไลด์ — ลองทำเองก่อนดูเฉลย

~25 นาที · ควรผ่าน บทเรียน 67 (map, filter, reduce) มาก่อน

สไลด์หน้า 39–43 ให้โจทย์มา 4 ข้อ — Grocery App · Movie App · Food Delivery · HR Payroll
ทั้งสี่ข้ออ่านแล้วดูง่ายมาก ประมาณ 3 บรรทัดจบ
แต่ทั้งสี่ข้อมีจุดที่โจทย์ไม่ได้บอก และถ้าคุณเดาเอง คุณกับคนตรวจงานจะได้คำตอบคนละอัน โดยที่โค้ดไม่ error สักตัว
บทนี้จึงทำสองอย่างพร้อมกัน: ให้คุณลองเขียนเอง (มีปุ่มเฉลยให้กดทีหลัง) และชี้ให้เห็นคำถามที่ต้องถามก่อนพิมพ์บรรทัดแรก
ปิดท้ายด้วย recap ของทั้งกลุ่ม TypeScript ตามสไลด์หน้า 44–48

🧰 ของที่ใช้ตรวจในบทนี้

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 ในบทนี้ก็อปมาจากหน้าจอจริง ไม่ได้เขียนจากความจำ
ข้อมูลตั้งต้นทุกชุดลอกมาจากสไลด์ตรง ๆ ไม่ได้แต่งเพิ่ม

วิธีใช้บทนี้ให้ได้ผลจริง

ลำดับที่ผมอยากให้ทำ ต่อโจทย์หนึ่งข้อ:

  1. อ่านโจทย์ + ดูข้อมูลตั้งต้น
  2. อ่านหัวข้อ "คำถามที่โจทย์ไม่ได้ตอบ" ก่อน แล้วตัดสินใจว่าคุณจะเลือกทางไหน — จดไว้
  3. เปิดไฟล์ .ts ของตัวเองแล้วเขียน
  4. รัน tsc --noEmit --strict ไฟล์.ts ให้ผ่านก่อน แล้วค่อยรันดูค่า
  5. ค่อยกดปุ่ม เฉลย
⚠️ กดเฉลยก่อนเขียน = อ่านเฉลยของคนอื่น ไม่ใช่ทำโจทย์

โค้ดพวกนี้อ่านแล้วเข้าใจง่ายมาก จนสมองหลอกว่า "เออ เราก็เขียนได้"
ความรู้สึกนั้นเชื่อไม่ได้ — วิธีเดียวที่จะรู้ว่าเขียนได้จริงคือพิมพ์มันออกมาแล้วให้ 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

  1. สร้างไฟล์ q1.ts วางข้อมูลตั้งต้นข้างบนลงไป
  2. เขียนฟังก์ชันที่คืน string[] ของชื่อของที่ available: true เป็นตัวพิมพ์ใหญ่
  3. รัน tsc --noEmit --strict q1.ts ให้ผ่าน
  4. เขียนบรรทัดพิสูจน์: 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

  1. เขียนฟังก์ชัน topRated(list, n) คืนหนัง n เรื่องคะแนนสูงสุด เรียงมากไปน้อย
  2. ห้ามให้ movies ตัวเดิมเปลี่ยนลำดับ
  3. รัน tsc --noEmit --strict ให้ผ่าน
  4. เขียนบรรทัดพิสูจน์: 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

  1. เขียนฟังก์ชันที่รับ Order[] แล้วคืน object ที่มีทั้งสามตัวเลข: ยอดรวมของที่ส่งแล้ว · เงินคืน · ยอดจ่ายจริง
  2. ประกาศ interface ของค่าที่คืนด้วย อย่าปล่อยให้ tsc เดาเอง
  3. เลือกเองว่า > หรือ >= แล้วเขียนคอมเมนต์กำกับว่าเลือกอันไหน
  4. เขียนบรรทัดพิสูจน์: ยิงข้อมูลอีกชุดที่ยอดรวม 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

  1. เขียนฟังก์ชันที่คืน Record<string, number> — key คือ role, value คือยอดรวมต่อเดือนของ full-time
  2. ใช้ reduce โดยใส่ type ให้ accumulator ชัด ๆ (ท่าที่อ่านง่ายสุดคือ .reduce<Record<string, number>>(...))
  3. รัน tsc --noEmit --strict ให้ผ่าน
  4. เขียนบรรทัดพิสูจน์: 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 ฟ้อง
โดยเฉพาะถ้าคุณเลือกตอบข้อกำกวมต่างจากในบท (เช่นเลือก >= แทน >) — นั่นไม่ใช่ความผิด แต่เป็นจุดที่ควรเอามาคุยกันว่าทำไมถึงเลือกแบบนั้น

อ้างอิง