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

TypeScript คืออะไร และ type พื้นฐานที่ต้องรู้

~20 นาที · ไม่ต้องเคยเขียน JavaScript มาก่อน · แต่ถ้าเคยเขียน Go มาแล้ว บทนี้จะชนกับความเคยชินของคุณอย่างน้อย 3 จุด

นี่คือบทแรกของกลุ่ม TypeScript คุณเพิ่งเขียน Go มา 9 วัน แล้วจู่ ๆ ก็ต้องมาเจอภาษาที่ หน้าตาเหมือนมี type แต่ type หายไปหมดตอนโปรแกรมทำงานจริง
บทนี้จะไม่เริ่มด้วยการท่องรายชื่อ type — เพราะรายชื่อมันสั้นมากจนน่าตกใจ (string · number · boolean จบ ไม่มี int ไม่มี float)
สิ่งที่บทนี้จะทำคือ คอมไพล์ไฟล์ .ts ให้ดูของจริง แล้วเปิดไฟล์ .js ที่ได้มากางให้เห็นกับตา ว่าคำว่า : number ที่คุณพิมพ์ลงไปนั้น หายไปไหน
ถ้าคุณเข้าใจแค่จุดเดียวจากบทนี้ ขอให้เป็นจุดนั้น เพราะมันคือรากของทุกความงงที่จะตามมาอีกสิบบท

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

tsc -v        → Version 7.0.2
node --version  → v24.14.0
# ตรวจ type:  tsc --noEmit --strict ไฟล์.ts
# คอมไพล์แล้วรัน: tsc --target es2022 --module nodenext --moduleResolution nodenext ไฟล์.ts && node ไฟล์.js

ทุก error code (TS2339 · TS2304 · …) และทุกบรรทัดที่เขียนว่า "ได้ผลว่า…" ในบทนี้ ก็อปมาจากผลรันจริงบนเครื่องผม ไม่ได้พิมพ์จากความจำ
คุณ copy โค้ดในกล่องไปรันซ้ำเองได้ทุกก้อน ถ้าได้ผลไม่ตรง มาบอกผมได้เลย

1 · TypeScript คืออะไร พูดแบบไม่ใช้ศัพท์

เว็บเบราว์เซอร์ทุกตัวในโลกรู้จักภาษาเดียวคือ JavaScript
JavaScript ไม่มีการประกาศ type เลย ตัวแปรตัวหนึ่งเก็บตัวเลขวันนี้ พรุ่งนี้เก็บข้อความก็ได้ ไม่มีใครห้าม ซึ่งสบายมากตอนเขียนไฟล์ 20 บรรทัด และเป็นนรกตอนโปรเจกต์มี 20,000 บรรทัด

TypeScript คือ JavaScript ที่ถูกเติม "ชั้นสำหรับเขียนบอกชนิดข้อมูล" เข้าไป
คุณเขียนไฟล์ .ts · มีเครื่องมือชื่อ tsc คอยอ่านไฟล์นั้นแล้วบ่นถ้าคุณใช้ข้อมูลผิดชนิด
แล้ว tsc จะแปลงไฟล์ .ts ของคุณเป็นไฟล์ .js ธรรมดา เพื่อเอาไปรันจริง

สไลด์หน้า 7 สรุปประโยชน์ไว้ 3 ข้อ ซึ่งตรงกับที่เจอจริง:

สไลด์บอกว่าแปลว่าในชีวิตจริงคือ
Tighter integration with your editor พิมพ์ user. แล้วเอดิเตอร์ขึ้นรายการช่องให้เลือก — เพราะมันรู้ว่า user มีช่องอะไรบ้าง
TypeScript code converts to JavaScript, which runs anywhere JavaScript runs ของที่รันจริงเป็น .js เสมอ เบราว์เซอร์ไม่เคยเห็นไฟล์ .ts ของคุณเลยสักครั้ง
Type inference to give you great tooling without additional code ส่วนใหญ่ไม่ต้องพิมพ์ type เอง — tsc เดาให้จากค่าที่คุณใส่

ตัวอย่างในสไลด์หน้า 6 คือโค้ดสี่บรรทัดที่มีจุดแดงอยู่ใต้คำว่า name — ผมเอามารันจริงเพื่อดูว่า tsc พูดว่าอะไรออกมาแน่:

const user = {
  firstName: "Angela",
  lastName: "Davis",
  role: "Professor",
};

console.log(user.name);
$ tsc --noEmit --strict 02-username.ts
02-username.ts(7,18): error TS2339: Property 'name' does not exist on type
'{ firstName: string; lastName: string; role: string; }'.

สังเกตสิ่งที่ tsc ทำได้โดยที่เราไม่ได้พิมพ์ type ให้มันเลยสักตัว — มันอ่านค่าที่เราใส่ แล้วสรุปเองว่าก้อนนี้มีสามช่อง ทุกช่องเป็น string
พอเราขอช่องที่สี่ที่ไม่มีอยู่จริง มันเลยฟ้อง
ใน JavaScript ล้วน ๆ บรรทัดนี้ไม่ error มันจะพิมพ์คำว่า undefined ออกมาเฉย ๆ แล้วคุณจะไปเจอตอนลูกค้าโทรมาถามว่าทำไมหน้าเว็บขึ้นคำว่า undefined

⚠️ tsc ฟ้องแล้ว แต่มันยังสร้างไฟล์ .js ให้อยู่ดี

ผมสั่งคอมไพล์ไฟล์ที่มี TS2339 ค้างอยู่นั่นแหละ แล้วดูว่าเกิดอะไรขึ้น:

$ tsc --target es2022 --module nodenext --moduleResolution nodenext 02-username.ts
02-username.ts(7,18): error TS2339: Property 'name' does not exist on type
'{ firstName: string; lastName: string; role: string; }'.
$ echo $?
2
$ ls 02-username.js
02-username.js          # ← ไฟล์ถูกสร้างขึ้นมาแล้ว ทั้งที่ยังมี error
$ node 02-username.js
undefined

นี่คือความต่างข้อแรกที่คน Go ต้องปรับ — go build ไม่ผ่านแปลว่าไม่มีไฟล์ให้รัน
แต่ tsc ฟ้องเสร็จก็ยังส่งไฟล์ออกมาให้ (แจ้ง exit code 2 ไว้แค่นั้น)
ในโปรเจกต์จริงจึงต้องให้ CI เป็นคนกันไว้ — สั่ง tsc --noEmit เป็นด่าน แล้วถ้า exit code ไม่ใช่ 0 ก็ไม่ให้ deploy

🧰 เทียบให้เห็นภาพ — คนพิสูจน์อักษรที่ยืนอยู่ก่อนถึงแท่นพิมพ์

โรงพิมพ์รับต้นฉบับมาพิมพ์เป็นหนังสือ แท่นพิมพ์ไม่สนใจว่าเนื้อหาสะกดถูกไหม มันพิมพ์ทุกอย่างที่ส่งเข้ามาเหมือนกันหมด
โรงพิมพ์ที่ดีเลยมี คนพิสูจน์อักษรยืนอยู่ก่อนถึงแท่น — เขาอ่านต้นฉบับ วงคำที่สะกดผิด ส่งกลับให้คุณแก้ก่อนที่กระดาษจะเสียไปหนึ่งหมื่นแผ่น

แท่นพิมพ์ = เบราว์เซอร์ (มันรัน JavaScript ทุกอย่างที่ส่งเข้ามา ไม่ถามอะไรเลย) · คนพิสูจน์อักษร = tsc
และสิ่งที่เข้าแท่นพิมพ์คือต้นฉบับที่ลบรอยวงสีแดงออกหมดแล้ว ไม่ใช่ต้นฉบับที่มีรอยวง

ที่ไม่เหมือนกัน: คนพิสูจน์อักษรตัวจริงจะไม่ยอมปล่อยงานถ้ายังมีคำผิด
แต่ tsc ปล่อยผ่านได้ — สั่งคอมไพล์ทั้งที่มี error มันก็ยังสร้างไฟล์ .js ให้อยู่ดี (ต้องสั่ง noEmitOnError เองถ้าไม่อยากให้ปล่อย)
แปลว่า "โค้ดรันได้" ไม่ได้แปลว่า "type ถูก" — สองเรื่องนี้แยกขาดจากกันโดยสิ้นเชิงใน TypeScript ซึ่งต่างจาก Go ที่ go build ไม่ผ่านแล้วคุณไม่มีไฟล์ให้รันเลย

🗣 ศัพท์ที่เพิ่งเรียนไป
tsc (TypeScript compiler)
โปรแกรมที่ทำสองหน้าที่ในตัวเดียว: ตรวจ type และ แปลง .ts เป็น .js · --noEmit = ตรวจอย่างเดียวไม่ต้องสร้างไฟล์ · พิมพ์บอก AI ได้ว่า “run tsc --noEmit --strict on this file and paste the exact error codes” จะได้คำตอบที่อ้างอิงของจริง ไม่ใช่คำตอบที่มันเดา
type inference
การที่ tsc เดา type ให้เองจากค่าที่คุณเขียน โดยคุณไม่ต้องพิมพ์บอก · เป็นเหตุผลว่าทำไมโค้ด TypeScript ในโปรเจกต์จริงถึงมี : type น้อยกว่าที่คุณคิด · ถาม senior ได้ว่า “ตรงนี้ปล่อยให้ infer หรือควรเขียน type ประกาศไว้” คำถามนี้มีคำตอบที่ต่างกันตามทีม ไม่ใช่เรื่องถูกผิด
--strict
สวิตช์รวมที่เปิดการตรวจเข้มทุกอย่างพร้อมกัน · โปรเจกต์ใหม่ควรเปิดตั้งแต่วันแรก เพราะเปิดทีหลังตอนโค้ดมีหมื่นบรรทัดคือฝันร้าย · ถาม senior ว่า “โปรเจกต์นี้ strict เปิดอยู่ไหม” ก่อนเขียนโค้ดบรรทัดแรก

2 · จุดที่ชนกับ Go แรงที่สุด — type หายไปตอนรัน

ใน Go type อยู่กับข้อมูลไปตลอดชีวิตของโปรแกรม ตอนรันอยู่คุณยังถามได้ว่า ตัวแปรนี้เป็นชนิดอะไร (reflect.TypeOf · type switch · type assertion)
ใน TypeScript ไม่มีอะไรแบบนั้นเหลืออยู่เลย เพราะ type ถูกลบทิ้งตอนแปลงเป็น .js

ไม่ต้องเชื่อผม ผมเขียนไฟล์นี้ขึ้นมาจริง ๆ:

const message: string = "Hello World";
const numeric: number = 1000;
const isEnabled: boolean = true;

type Score = { name: string; score: number };

const scoreList: Array<number> = [40, 45, 50, 43, 50];

const johnDoeScore: Score = { name: "John Doe", score: 40 };

function total(list: Array<number>): number {
  return list.reduce((a: number, b: number) => a + b, 0);
}

console.log(message, numeric, isEnabled);
console.log(total(scoreList), johnDoeScore.name);

สั่งคอมไพล์ แล้วเปิดไฟล์ .js ที่มันสร้างให้ ออกมาหน้าตาแบบนี้เป๊ะ ๆ:

$ tsc --target es2022 --module nodenext --moduleResolution nodenext 01-erase.ts
$ cat 01-erase.js
const message = "Hello World";
const numeric = 1000;
const isEnabled = true;
const scoreList = [40, 45, 50, 43, 50];
const johnDoeScore = { name: "John Doe", score: 40 };
function total(list) {
    return list.reduce((a, b) => a + b, 0);
}
console.log(message, numeric, isEnabled);
console.log(total(scoreList), johnDoeScore.name);
export {};

วางสองไฟล์เทียบกันแล้วไล่ดูว่าอะไรหายไปบ้าง:

ไฟล์ .ts ที่เราเขียน (16 บรรทัด)

const message: string = "Hello World";
type Score = { name: string; score: number };
const scoreList: Array<number> = [40, ...];
function total(list: Array<number>): number {

มี : type กำกับอยู่ 9 จุด · บวกบรรทัด type Score อีกเต็มบรรทัด

ไฟล์ .js ที่ได้ (11 บรรทัด)

const message = "Hello World";
(บรรทัด type Score หายไปทั้งบรรทัด)
const scoreList = [40, ...];
function total(list) {

เหลือ type ศูนย์จุด · เหมือนไม่เคยมีใครพิมพ์มันลงไป

ผลรันของไฟล์ .js คือ Hello World 1000 true แล้วบรรทัดถัดมา 228 John Doe
เหมือนกันเป๊ะกับตอนที่ยังมี type อยู่ เพราะ type ไม่เคยมีส่วนร่วมกับการคำนวณอะไรเลย

แล้วถ้าลองถามหา type ตอนรันล่ะ ก็จะเจอแบบนี้:

type Score = { name: string; score: number };
const johnDoeScore: Score = { name: "John Doe", score: 40 };

console.log(typeof johnDoeScore);
console.log(johnDoeScore instanceof Score);
$ tsc --noEmit --strict 07-runtime.ts
07-runtime.ts(5,37): error TS2693: 'Score' only refers to a type, but is being used as a value here.

ประโยค "'Score' only refers to a type" คือคำอธิบายที่ตรงที่สุดของทั้งบทนี้ — Score ไม่ใช่ "ของ" ที่มีตัวตนในโปรแกรม มันเป็นแค่หมายเหตุที่คุณเขียนให้ tsc อ่าน
ส่วนบรรทัดบน typeof johnDoeScore ตอนรันจริงได้คำว่า object เฉย ๆ ไม่ใช่ Score เพราะนั่นคือ typeof ของ JavaScript ซึ่งรู้จักแค่ string · number · boolean · object · และอีกไม่กี่คำ

แล้วถ้าดันทุรังคอมไพล์ไฟล์นี้แล้วรันเลยล่ะ — tsc ก็ยังส่งไฟล์ .js ออกมาให้เหมือนเดิม (ตามที่เตือนไว้ในหัวข้อ 1) แล้ว node ก็เจอของจริงเข้าไปเต็ม ๆ:

$ tsc --target es2022 --module nodenext --moduleResolution nodenext 07-runtime.ts
07-runtime.ts(5,37): error TS2693: 'Score' only refers to a type, but is being used as a value here.
$ node 07-runtime.js
object
console.log(johnDoeScore instanceof Score);
                                    ^

ReferenceError: Score is not defined
    (stack trace ที่เหลือตัดออก)

บรรทัดแรกพิมพ์ object ออกมาได้ตามปกติ แล้วบรรทัดที่สองพังทันที
เปิดไฟล์ .js ดูจะเข้าใจเลยว่าทำไม — type Score ถูกลบทิ้งไปแล้ว เหลือแต่คำว่า Score ลอย ๆ ที่ไม่มีตัวตนอะไรรองรับ ตอน node อ่านถึง
นี่คือเหตุผลที่ tsc ห้ามไว้ตั้งแต่ตอนตรวจ: มันรู้ล่วงหน้าว่าโค้ดบรรทัดนี้จะไม่มีทางรันผ่าน

function addTen(n: number): number {
  return n + 10;
}

const fromOutside: any = "5";   // สมมติว่าค่านี้มาจาก API ข้างนอก

const answer: number = addTen(fromOutside);

console.log(answer);
console.log(typeof answer);

tsc --noEmit --strict จะฟ้องกี่ error? แล้วสองบรรทัดสุดท้ายพิมพ์อะไรออกมาตอนรันจริง?
(ตัวแปรชื่อ answer ประกาศไว้ว่าเป็น number นะครับ)

tsc ฟ้อง 0 error — ผ่านฉลุย

$ tsc --noEmit --strict 09-lie2.ts
(ไม่มีอะไรออกมา — exit code 0)

$ node 09-lie2.js
510
string

ตัวแปรที่เขียนกำกับไว้ชัด ๆ ว่า : number เก็บค่า "510" ซึ่งเป็น string
เกิดอะไรขึ้น: "5" + 10 ใน JavaScript ไม่ใช่การบวก แต่เป็นการต่อข้อความ ได้ "510"
แล้วทำไม tsc ไม่ฟ้อง: เพราะเราประกาศ fromOutside เป็น any ซึ่งแปลว่า "อย่าตรวจตัวนี้" — tsc ก็เชื่อฟังอย่างซื่อสัตย์

บทเรียนของก้อนนี้: : number ไม่ใช่คำสั่งให้ระบบบังคับค่า มันคือคำสัญญาที่คุณให้ไว้กับ tsc ว่าตรงนี้จะเป็นตัวเลข
ถ้าคุณโกหก (ด้วย any หรือด้วยข้อมูลจาก API ที่หน้าตาไม่ตรงจริง) ไม่มีใครจับได้ตอนรัน เพราะไม่มีใครเหลืออยู่ให้จับ
นี่คือสิ่งที่ Go ไม่ยอมให้เกิด — ใน Go ค่าที่ประกาศเป็น int จะไม่มีวันกลายเป็น string ได้เลยแม้แต่ตอนรัน

🧰 เทียบให้เห็นภาพ — นั่งร้านของช่างก่อสร้าง

ตึกที่กำลังสร้างจะมีนั่งร้านล้อมรอบ ช่างใช้มันเดิน ใช้ยกของ ใช้ตรวจว่าผนังตรงไหม นั่งร้านสำคัญมากระหว่างก่อสร้าง — ไม่มีมันงานเดินไม่ได้เลย
แต่วันเปิดตึก นั่งร้านถูกรื้อออกหมด ไม่มีชิ้นไหนเหลือติดตัวตึก

นั่งร้าน = type ของ TypeScript · ตึกที่เปิดใช้ = ไฟล์ .js ที่รันจริง
คำถามที่มือใหม่ถามบ่อยที่สุดคือ "ถ้ามันหายไปหมด แล้วจะเขียนไปทำไม" — คำตอบเดียวกับที่ถามว่าจะสร้างนั่งร้านไปทำไมทั้งที่ต้องรื้อ: มันไม่ได้มีไว้อยู่ถาวร มันมีไว้ให้งานตอนสร้างไม่พัง

ที่ไม่เหมือนกัน: ตึกที่สร้างเสร็จแล้วแข็งแรงอยู่ได้ด้วยตัวเอง ต่อให้รื้อนั่งร้านไป ผนังก็ยังตรงเหมือนวันที่ช่างวัด
แต่ไฟล์ .js ที่ได้จาก TypeScript ไม่ได้แข็งแรงขึ้นเลยแม้แต่นิดเดียว — มันคือ JavaScript ที่ยอมรับทุกอย่างเหมือนเดิมทุกประการ
ข้อมูลหน้าตาแปลก ๆ ที่มาจาก API ตอนตีสามยังทำให้มันพังได้เท่าเดิม เพราะความปลอดภัยทั้งหมดอยู่ที่ตอนตรวจ ไม่ได้อยู่ในตัวโปรแกรม

🗣 ศัพท์ที่เพิ่งเรียนไป
compile time / runtime
สองช่วงเวลาที่แยกขาดกัน — compile time คือตอนที่ tsc อ่านโค้ด · runtime คือตอนที่ node หรือเบราว์เซอร์รันโค้ด · ใน TypeScript type มีชีวิตอยู่แค่ช่วงแรกเท่านั้น · ถาม senior ได้ว่า “บั๊กนี้เป็น compile-time error หรือ runtime error” คำตอบจะบอกทันทีว่าต้องไปแก้ที่ type หรือต้องไปเขียนโค้ดตรวจค่าจริง ๆ
type erasure
การที่ type ถูกลบทิ้งตอนแปลงเป็นภาษาที่รันจริง ไม่เหลือร่องรอยใน output · เป็นคำที่ใช้อธิบายได้ตรงที่สุดว่าทำไม instanceof Score ถึงเขียนไม่ได้ · พิมพ์ถาม AI ได้ว่า “TypeScript types are erased at runtime — how do I validate this API response at runtime instead?” แล้วมันจะพาไปเรื่อง validation library แทนที่จะพยายามใช้ type
any
type ที่แปลว่า "เลิกตรวจตัวนี้" — ค่าที่เป็น any เอาไปใส่ตรงไหนก็ได้โดยไม่มีใครฟ้อง · มันไม่ใช่ "ชนิดอะไรก็ได้" แต่คือการปิดสวิตช์ความปลอดภัยเฉพาะจุด · เห็น any ในโค้ดคนอื่นให้ถามว่า “ตรงนี้จำเป็นต้อง any จริงไหม หรือแค่ยังไม่มีเวลาเขียน type”

3 · primitive สามตัว — และช่องว่างตรงที่เคยมี int

สไลด์หน้า 10 ให้มาสามบรรทัด ผมรันแล้วผ่านทั้งหมด:

const message: string = "Hello World";
const numeric: number = 1000;
const isEnabled: boolean = true;

ประโยคที่ต้องอ่านช้า ๆ อยู่ในสไลด์หน้าเดียวกัน: "Number represents for number like 100 or 100.5, no equivalent int or float. Everything is number."

ผมลองพิมพ์ int กับ float ลงไปตรง ๆ ดูว่าเกิดอะไรขึ้น:

const age: int = 30;
const price: float = 19.99;
const qty: number = "10";
$ tsc --noEmit --strict 04-noint.ts
04-noint.ts(1,12): error TS2304: Cannot find name 'int'.
04-noint.ts(2,14): error TS2552: Cannot find name 'float'. Did you mean 'GLfloat'?
04-noint.ts(3,7): error TS2322: Type 'string' is not assignable to type 'number'.
⚠️ อย่าหลงกับคำว่า GLfloat ในบรรทัดที่สอง

tsc พยายามช่วยเดาว่าคุณอาจพิมพ์ผิด แล้วไปเจอชื่อ GLfloat ที่ใกล้เคียงที่สุดในคลังชื่อของมัน
ตัวนั้นเป็นของ WebGL (ระบบวาดกราฟิก 3 มิติในเบราว์เซอร์) ไม่ใช่ชนิดตัวเลขของภาษา และไม่ใช่สิ่งที่คุณกำลังหา
บทเรียนที่ใช้ได้กับทุกภาษา: ข้อความ "Did you mean…" คือการเดาจากการสะกดที่คล้ายกัน ไม่ใช่จากความหมาย เชื่อบรรทัด TS2304 ที่บอกว่า "ไม่มีชื่อนี้" ดีกว่า

ทีนี้ประเด็นสำคัญ: ถ้ามี number ตัวเดียว แล้วจำนวนเต็มกับทศนิยมอยู่ด้วยกันยังไง
คำตอบคือ ทุกตัวเลขใน JavaScript คือทศนิยม 64 บิตแบบเดียวกันหมด เลข 1000 ที่คุณเขียน จริง ๆ แล้วเก็บอยู่ในรูปแบบเดียวกับ 100.5 เป๊ะ ๆ
ผลที่ตามมาซึ่งคน Go ต้องรู้ ผมรันให้ดูสามบรรทัด:

const a: number = 10;
const b: number = 3;

console.log(a / b);
console.log(0.1 + 0.2);
console.log(typeof a);
$ node 03-number.js
3.3333333333333335
0.30000000000000004
number
สิ่งที่คุณเคยทำใน Goใน TypeScriptสิ่งที่เปลี่ยนไปจริง ๆ
var x int = 10 / 3 ได้ 3 10 / 3 ได้ 3.3333333333333335 ไม่มีการหารแบบปัดทิ้ง · อยากได้ 3 ต้องเรียก Math.trunc() เอง
int8 int16 int32 int64 uint… ไม่มีสักตัว มีแต่ number เลือกขนาดไม่ได้ และไม่ต้องเลือก · แลกกับความแม่นยำที่หายไปเมื่อเลขใหญ่มาก
float64 ทศนิยม number ตัวเดียวกัน 0.1 + 0.2 ได้ 0.30000000000000004 — เป็นทศนิยมฐานสอง 64 บิตเหมือน float64
แต่ถ้าไปลองใน Go แล้วได้ไม่ตรงกัน อ่านกล่องข้างล่างก่อน
คอมไพเลอร์ห้ามบวก int กับ float64 บวกกันได้หมด เพราะเป็นชนิดเดียวกัน เขียนง่ายขึ้นจริง · แต่เสียตัวช่วยเตือนตอนที่คุณเผลอเอาจำนวนคนไปหารแล้วได้เศษ
⚠️ ลองใน Go แล้วได้ 0.3 ไม่ใช่ 0.30000000000000004 — ไม่ได้แปลว่า Go แม่นกว่า

ผมรันทั้งสองท่าใน Go ให้ดู เพราะจุดนี้หลอกคนได้จริง:

fmt.Println(0.1 + 0.2)   // → 0.3

a := 0.1
b := 0.2
fmt.Println(a + b)       // → 0.30000000000000004

ท่าแรกได้ 0.3 เพราะ Go มองเลขที่พิมพ์สดลงไปตรง ๆ เป็น untyped constant — มันบวกกันด้วยความละเอียดไม่จำกัดตั้งแต่ตอนคอมไพล์ แล้วค่อยแปลงผลลัพธ์เป็น float64 ทีเดียวตอนท้าย
ท่าที่สองคือของจริงที่เทียบกับ TypeScript ได้ — พอค่าลงไปนั่งในตัวแปร float64 แล้วค่อยบวก ก็ได้ 0.30000000000000004 เท่ากับ TypeScript เป๊ะ
สรุป: ตัวเลขทศนิยมของสองภาษาเก็บเหมือนกันทุกบิต · ที่ต่างคือ Go มีขั้นตอนคำนวณค่าคงที่ตอนคอมไพล์ ส่วน JavaScript ไม่มี ทุกการบวกใน TypeScript จึงเกิดตอนรันเสมอ

📌 แล้วเงินล่ะ เก็บใน number ได้ไหม

0.1 + 0.2 ที่ไม่เท่ากับ 0.3 คือเหตุผลที่ห้ามเก็บจำนวนเงินเป็นทศนิยม — ทั้งใน TypeScript และใน Go
ท่ามาตรฐานคือเก็บเป็นหน่วยเล็กที่สุดในรูปจำนวนเต็ม (เก็บเป็นสตางค์ ไม่ใช่บาท) แล้วค่อยหารตอนแสดงผลอย่างเดียว
ข้อนี้ไม่ได้อยู่ในสไลด์ แต่มันคือกับดักที่รอคุณอยู่ในทุกโปรเจกต์ที่มีราคาสินค้า

4 · array — ล็อกไว้ว่ากล่องนี้ใส่อะไรได้

สไลด์หน้า 11 ให้มาสองบรรทัด:

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> อ่านว่า "array ที่ข้างในเป็น number" — ส่วน <…> คือช่องให้ใส่ว่าข้างในเป็นอะไร
เขียนอีกแบบที่เจอบ่อยกว่าในโค้ดจริงคือ number[] ซึ่งเหมือนกันทุกประการ ต่างกันแค่รูปแบบการพิมพ์

ลองใส่ของผิดชนิดลงไปดูว่าเจออะไร:

const scoreList: Array<number> = [40, 45, 50, 43, 50];
scoreList.push(60);
scoreList.push("70");

const mixed: Array<number> = [40, "45", 50];
$ tsc --noEmit --strict 05-array.ts
05-array.ts(3,16): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
05-array.ts(5,35): error TS2322: Type 'string' is not assignable to type 'number'.

สังเกตว่าสองบรรทัดนี้ผิดเรื่องเดียวกันแต่ได้เลข error คนละตัว — TS2345 ใช้เมื่อส่งค่าผิดเข้าฟังก์ชัน (คำว่า parameter ในข้อความบอกไว้) ส่วน TS2322 ใช้เมื่อยัดค่าผิดลงตัวแปร (คำว่า assignable)
รู้ความต่างนี้ไว้ช่วยเวลาค้นหา error ตอนดึก ๆ ได้จริง

✅ ตรงนี้ TypeScript เข้มกว่าที่คน Go คาด

ใน Go เวลาจะเก็บของหลายชนิดในสไลซ์เดียว คุณต้องใช้ []any แล้วมานั่ง type assert ทีหลัง
ใน TypeScript array ที่ปนชนิดโดนฟ้องตั้งแต่บรรทัดที่ประกาศ ถ้าคุณระบุ type ไว้
บรรทัดที่สไลด์เขียนว่า "Use specific types only" หมายถึงเรื่องนี้ — อย่าเผลอปล่อยให้กลายเป็น Array<any> เพราะนั่นคือการกลับไปเป็น JavaScript เปล่า ๆ

5 · object type — และกฎที่คน Go ต้องเลิกเชื่อหนึ่งข้อ

สไลด์หน้า 12 บรรยายก้อนข้อมูลของนักเรียนสามคน แล้วเก็บรวมไว้ใน array ก้อนเดียว:

const johnDoeScore = {
  name: "John Doe",
  score: 40,
};

const scores: Array<{ name: string; score: number }> = [
  johnDoeScore,
  { name: "Frederic Castro", score: 45 },
  { name: "Josie Watson", score: 50 },
];

ก้อน { name: string; score: number } คือ object type — คำอธิบายรูปร่างของก้อนข้อมูล ว่ามีช่องอะไร ช่องละชนิดไหน
คั่นแต่ละช่องด้วย เซมิโคลอน (ในตัวอธิบาย type) ส่วนตอนใส่ค่าจริงคั่นด้วย จุลภาค — จุดนี้พิมพ์สลับกันบ่อยมากตอนเริ่มต้น

เติมเครื่องหมาย ? ท้ายชื่อช่อง แปลว่า "ช่องนี้จะมีหรือไม่มีก็ได้" ตามที่สไลด์เขียนว่า Use optional properties
(ก้อน scores ใช้ตัวเดิมจากบล็อกบน ผมตัดออกให้อ่านง่าย):

const johnDoeScore: { name: string; score: number; note?: string } = {
  name: "John Doe",
  score: 40,
};

console.log(scores.length, johnDoeScore.note);
console.log(scores.map((s) => s.score).join(","));
$ tsc --noEmit --strict 11-objtype.ts   (ผ่าน 0 error)
$ node 11-objtype.js
3 undefined
40,45,50

ช่อง note ที่ไม่ได้ใส่ค่า อ่านออกมาได้ undefined ตอนรัน — ไม่ใช่สตริงว่าง
นี่ต่างจาก Go ที่ field ของ struct ซึ่งไม่ได้กำหนดค่าจะได้ zero value ("" สำหรับ string) ใน TypeScript ช่องที่ไม่มีคือไม่มีจริง ๆ ไม่ได้ถูกเติมค่าให้

กฎที่ต้องเลิกเชื่อ: ชื่อ type ไม่ได้เป็นตัวกำหนดว่าเข้ากันได้

ใน Go ถ้าคุณประกาศ type Score struct{…} กับ type Player struct{…} ที่มี field เหมือนกันเป๊ะ คุณก็ยังเอาตัวหนึ่งไปใส่ที่ของอีกตัวไม่ได้ ถ้าไม่แปลงชนิดก่อน เพราะ Go ดูที่ชื่อ
TypeScript ดูที่รูปร่าง ผมรันพิสูจน์:

type Score = { name: string; score: number };
type Player = { name: string; score: number };

const s: Score = { name: "John Doe", score: 40 };
const p: Player = s;      // ← Go ไม่ยอม แต่ TypeScript ยอม

type Point = { x: number; y: number };
const notAPoint: Point = { x: 1 };
$ tsc --noEmit --strict 06-structural.ts
06-structural.ts(8,7): error TS2741: Property 'y' is missing in type '{ x: number; }' but required in type 'Point'.

มี error แค่ข้อเดียว และไม่ใช่บรรทัด const p: Player = s;
TypeScript ถือว่า Score กับ Player เป็นสิ่งเดียวกัน เพราะรูปร่างตรงกันทุกช่อง ชื่อไม่มีความหมายอะไรเลย
ส่วน Point ที่ขาดช่อง y ถึงจะโดนฟ้อง เพราะรูปร่างไม่ตรงจริง ๆ

แต่มีข้อยกเว้นหนึ่งที่ทำให้คนงงบ่อยมาก ผมรันเทียบสองท่าที่ดูเหมือนกัน:

type Score = { name: string; score: number };

// ท่าที่ 1 — ใส่ก้อนตรง ๆ ตอนประกาศ
const direct: Score = { name: "John Doe", score: 40, grade: "A" };

// ท่าที่ 2 — พักไว้ในตัวแปรก่อน แล้วค่อยยัดเข้า Score
const box = { name: "Josie Watson", score: 50, grade: "A" };
const viaVariable: Score = box;
$ tsc --noEmit --strict 10-excess.ts
10-excess.ts(4,54): error TS2353: Object literal may only specify known properties,
and 'grade' does not exist in type 'Score'.
⚠️ ก้อนเดียวกัน ช่องเกินเหมือนกัน แต่ฟ้องแค่ท่าเดียว

ท่าที่ 1 โดนฟ้อง · ท่าที่ 2 ผ่านเงียบ ๆ
กฎที่ TypeScript ใช้คือ: ก้อนที่คุณเพิ่งพิมพ์สด ๆ ตรงนั้น จะถูกตรวจเข้มเป็นพิเศษว่ามีช่องเกินไหม เพราะช่องเกินในก้อนที่เพิ่งพิมพ์ มักแปลว่าคุณพิมพ์ชื่อช่องผิด (เช่นตั้งใจพิมพ์ score แต่พิมพ์ scores)
ส่วนก้อนที่ผ่านตัวแปรมาแล้ว ถือว่า "มีมากกว่าที่ขอ ก็ยังใช้ได้" ตามหลักรูปร่าง
ผลกระทบจริง: ข้อมูลที่มาจาก API แล้วมีช่องเกินมา จะไม่มีใครฟ้อง เพราะมันไม่ใช่ก้อนที่คุณพิมพ์เอง — อย่าคาดหวังว่า TypeScript จะช่วยกรองช่องแปลกปลอมออกให้

🧰 เทียบให้เห็นภาพ — ยามที่ดูบัตร กับ ยามที่ดูเครื่องแบบ

ตึกสองตึกมีกฎเข้าออกคนละแบบ:
· ตึก A ยามขอดูบัตรพนักงาน ต้องเป็นบัตรที่ตึกนี้ออกให้เท่านั้น ต่อให้คุณใส่ชุดเหมือนพนักงานทุกอย่าง ไม่มีบัตรก็ไม่ให้เข้า
· ตึก B ยามดูแค่ว่าคุณใส่ชุดครบตามที่กำหนดไหม — เสื้อกาวน์ ถุงมือ หมวก ครบก็เข้าได้ ไม่ถามว่าคุณมาจากบริษัทไหน

ตึก A = Go (ดูชื่อ type) · ตึก B = TypeScript (ดูรูปร่าง)
นี่คือเหตุผลที่ const p: Player = s; ผ่านฉลุย — s ใส่ชุดครบทุกชิ้นที่ Player ต้องการ

ที่ไม่เหมือนกัน: ยามตึก B ยังเห็นคุณเดินเข้ามา ตอนที่คุณเดินเข้ามาจริง ๆ ถ้าวันหนึ่งมีคนใส่ชุดไม่ครบเดินผ่าน เขาจะทัก
แต่ยามของ TypeScript เลิกงานตั้งแต่ก่อนตึกเปิด — เขาตรวจแค่รายชื่อคนที่วางแผนจะเข้า ส่วนตอนตึกเปิดจริง ประตูไม่มีคนเฝ้าเลย
ข้อมูลที่มาจาก API ตอนรัน จึงเดินเข้ามาได้ทุกหน้าตา ไม่ว่ารูปร่างจะตรงกับที่คุณเขียนไว้หรือไม่

🗣 ศัพท์ที่เพิ่งเรียนไป
structural typing / nominal typing
structural = เข้ากันได้ถ้ารูปร่างตรง (TypeScript) · nominal = เข้ากันได้ต่อเมื่อชื่อตรง (Go, Java) · พูดกับ senior ว่า “TypeScript เป็น structural typing” จะสื่อได้ทันที ว่าทำไม type สองตัวที่ชื่อคนละชื่อถึงสลับกันได้
object type
คำอธิบายรูปร่างของก้อนข้อมูล เขียนเป็น { ช่อง: ชนิด; ช่อง: ชนิด } · เขียนคาไว้ตรงนั้นก็ได้ หรือตั้งชื่อด้วย type ก็ได้ (บทถัดไปจะเทียบกับ interface) · พิมพ์บอก AI ได้ว่า “give me the object type for this JSON response” แล้ววาง JSON จริงลงไป
optional property (?)
ช่องที่มีหรือไม่มีก็ได้ · ถ้าไม่มี อ่านค่าออกมาได้ undefined · ต่างจาก zero value ของ Go ที่ช่องนั้นมีอยู่เสมอ แค่ค่าว่าง · ถาม senior ว่า “ช่องนี้ optional หรือ required” ก่อนเขียน type เพราะเลือกผิดแล้วโค้ดที่ใช้ต่อจะต้องแก้ทั้งหมด
excess property check
การตรวจเข้มพิเศษที่ใช้กับก้อนข้อมูลที่พิมพ์สดตรงนั้น ว่ามีช่องเกินไหม · เจอ TS2353 แล้วอยากเข้าใจว่าทำไมท่าอื่นไม่ฟ้อง ให้ค้นคำนี้ตรง ๆ จะเจอคำอธิบายเร็วกว่าค้นด้วยข้อความ error

6 · ภารกิจ — พิสูจน์ด้วยมือตัวเองว่า type หายไปจริง

🎯 ทำสามรอบนี้ให้ครบ แล้วคุณจะไม่ลืมบทนี้อีกเลย

สร้างโฟลเดอร์เปล่าหนึ่งอัน แล้วสร้างไฟล์ package.json ที่มีข้อความว่า { "type": "module" } อยู่ข้างใน (บรรทัดเดียวจบ) จากนั้น:

  1. รอบที่ 1 — ทำให้ tsc ฟ้องให้ได้ 3 แบบ
    เขียนไฟล์ lab.ts ที่มีความผิดสามอย่างพร้อมกัน: ประกาศตัวแปรเป็น int · ใส่ string ลงใน Array<number> · และอ่านช่องที่ object ไม่มี
    รัน tsc --noEmit --strict lab.ts แล้วจดเลข TS ทั้งสามตัวลงกระดาษ พร้อมบอกว่าเลขไหนคู่กับความผิดข้อไหน
  2. รอบที่ 2 — เปิดฝาดูว่า type หายไปตรงไหน
    แก้ไฟล์ให้ไม่มี error แล้วใส่ type ที่ตั้งชื่อเองอย่างน้อยหนึ่งอัน กับ : string : number อย่างน้อยอย่างละหนึ่งจุด
    รัน tsc --target es2022 --module nodenext --moduleResolution nodenext lab.ts แล้ว cat lab.js
    นับให้ได้ว่าไฟล์ .ts มี type กี่จุด และไฟล์ .js เหลือกี่จุด
  3. รอบที่ 3 — เขียนโค้ดที่ผ่านการตรวจ แต่โกหก (ข้อนี้คือข้อสอบตัวจริง)
    ทำให้มีตัวแปรที่ประกาศว่า : number แต่ตอนรันจริง typeof ของมันไม่ใช่ number โดยที่ tsc --noEmit --strict ต้องผ่าน 0 error
    เงื่อนไขการผ่าน: คุณต้องแปะได้ทั้ง (ก) ผลรัน tsc ที่ไม่มี error และ (ข) ผลรัน node ที่พิมพ์ชนิดจริงออกมาแล้วมันไม่ตรงกับที่ประกาศ
    ถ้าทำข้อนี้ได้ แปลว่าคุณเข้าใจแล้วว่า type ของ TypeScript รับประกันอะไร และไม่รับประกันอะไร

กุญแจอยู่ที่คำว่า any — มันคือประตูเดียวที่ tsc เปิดทิ้งไว้ให้ค่าผิดชนิดเดินผ่าน
โครงที่ใช้ได้ (ผมรันแล้วได้ 510 กับ string):

function addTen(n: number): number { return n + 10; }
const fromOutside: any = "5";
const answer: number = addTen(fromOutside);
console.log(answer, typeof answer);

ถ้าอยากท้าทายตัวเองต่อ: ลองทำแบบไม่ใช้คำว่า any เลยดู
ใบ้: JSON.parse() คืนค่าที่ tsc ยอมให้เป็นอะไรก็ได้อยู่แล้ว และนั่นคือช่องทางที่ข้อมูลจาก API เดินเข้าโปรแกรมคุณทุกวันในงานจริง

7 · ทดสอบความเข้าใจ

1. โค้ดนี้ผ่าน tsc --noEmit --strict ทั้งที่ answer ประกาศเป็น number: const fromOutside: any = "5"; const answer: number = addTen(fromOutside); — เพราะอะไร?

ผลรันจริงคือ tsc ไม่ฟ้องอะไรเลย แล้ว node พิมพ์ 510 กับ string ออกมา
ตัวเลือกที่ 1 ผิด — นี่เป็นพฤติกรรมที่ออกแบบมาแบบนี้ตั้งใจ ไม่ใช่บั๊ก any มีไว้ให้ค่อย ๆ ย้ายโค้ด JavaScript เก่ามาเป็น TypeScript ได้ทีละส่วน
ตัวเลือกที่ 2 ผิด — เปิดไฟล์ .js ที่ได้ดูจะเห็นว่า const fromOutside = "5"; ยังเป็นสตริงอยู่เหมือนเดิม tsc ไม่เคยแปลงค่าให้ใคร มันแค่ลบ type ออกอย่างเดียว
ตัวเลือกที่ 4 ผิดสองชั้น — ทั้งไม่มีการบังคับค่าตอนรัน และเลขก็ไม่ใช่ 15 เพราะ "5" + 10 ใน JavaScript คือการต่อข้อความ ได้ "510"
จุดที่ต้องจำ: : number คือคำสัญญาที่คุณให้ไว้กับ tsc ไม่ใช่ยามที่ยืนเฝ้าตอนโปรแกรมทำงาน ถ้าข้อมูลมาจากนอกโปรแกรม คุณต้องเขียนโค้ดตรวจเองตอนรัน

2. เขียน const age: int = 30; ในไฟล์ .ts แล้วรัน tsc --noEmit --strict จะได้อะไร?

ผลรันจริงคือ 04-noint.ts(1,12): error TS2304: Cannot find name 'int'.
คำว่า "Cannot find name" สำคัญมาก — มันไม่ได้แปลว่า "ใส่ค่าผิดชนิด" แต่แปลว่า ไม่มีชื่อนี้อยู่ในภาษาเลย เหมือนคุณพิมพ์ชื่อ type ที่คิดขึ้นมาเอง
ตัวเลือกที่ 1 ผิด — TypeScript มีชนิดตัวเลขตัวเดียวคือ number ไม่มีชื่อย่อ ไม่มี int8 int64 อะไรทั้งนั้น
ตัวเลือกที่ 3 ผิด — ไฟล์ .js ที่ได้ไม่มีคำว่า int เหลืออยู่เลย node จึงไม่มีทางพังด้วยเหตุนี้
ตัวเลือกที่ 4 ผิดเพราะเลือก error code ผิดตัว — TS2322 ใช้ตอนที่ทั้งสองฝั่งเป็นชนิดที่มีอยู่จริงแต่ไม่เข้ากัน เช่นเอา string ใส่ number
จุดที่ต้องจำ: TS2304 = "ไม่รู้จักชื่อนี้" · TS2322 = "รู้จักทั้งคู่ แต่ยัดใส่กันไม่ได้" อ่านความต่างนี้ออกจะประหยัดเวลาไล่บั๊กได้มาก

3. ประกาศ type Score = { name: string; score: number } กับ type Player = { name: string; score: number } แล้วเขียน const p: Player = s; โดย s เป็น Score — เกิดอะไรขึ้น?

ผมรันไฟล์นี้จริง มี error ออกมาข้อเดียวคือ TS2741 ที่บรรทัดอื่น (บรรทัด Point ที่ขาดช่อง y) ส่วนบรรทัด const p: Player = s; ไม่ถูกฟ้องเลย
ตัวเลือกที่ 1 คือคำตอบที่คนมาจาก Go จะเลือกมากที่สุด และมันถูกสำหรับ Go — แต่ผิดสำหรับ TypeScript ซึ่งเป็น structural typing
ตัวเลือกที่ 2 ผิด — ประกาศ type ที่รูปร่างซ้ำกันกี่อันก็ได้ ไม่มีใครห้าม
ตัวเลือกที่ 3 ผิด — ตอนรันไม่มีการ "แปลงชนิด" เกิดขึ้นเลยแม้แต่นิดเดียว เพราะไฟล์ .js ที่ได้เหลือแค่ const p = s; ซึ่งคือการชี้ไปที่ก้อนเดิม
จุดที่ต้องจำ: ชื่อ type ใน TypeScript มีไว้ให้คนอ่าน ไม่ได้มีไว้ให้คอมไพเลอร์แบ่งแยก ถ้าอยากให้สองอย่างที่รูปร่างเหมือนกันสลับกันไม่ได้จริง ๆ ต้องใช้ท่าพิเศษ (branded type) ซึ่งไม่ใช่ของมาตรฐาน

4. scoreList เป็น Array<number> แล้วเรียก scoreList.push("70") ได้ TS2345 ส่วน const mixed: Array<number> = [40, "45", 50] ได้ TS2322 — ทำไมคนละเลข?

อ่านข้อความเต็มของทั้งสองบรรทัดจะเห็นคำที่ต่างกันชัด ๆ:
TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
TS2322: Type 'string' is not assignable to type 'number'.
คำว่า parameter บอกว่ากำลังพูดถึงการส่งค่าเข้าฟังก์ชัน (push ก็คือฟังก์ชัน)
ตัวเลือกที่ 2 ผิด — Array<number> กับ number[] เป็นสิ่งเดียวกันเป๊ะ เขียนต่างกันได้แต่ไม่มีผลกับ error code
ตัวเลือกที่ 3 ผิด — ทั้งคู่เป็น error เท่ากัน tsc ไม่มีระดับ "คำเตือน" แยกออกมาแบบนั้น
ตัวเลือกที่ 4 ผิดฐานรากที่สุด — error ของ tsc ทุกตัวเกิดตอนตรวจ ไม่มีตัวไหนเกิดตอนรัน เพราะตอนรันไม่มี type เหลืออยู่ให้ตรวจแล้ว
จุดที่ต้องจำ: อ่านคำในข้อความ error ไม่ใช่แค่เลข — คำว่า parameter · property · assignable บอกได้ว่าต้องไปแก้ตรงไหน เร็วกว่าเอาเลขไปค้นกูเกิล

อ้างอิง

ติดตรงไหนถามได้เลยครับ โดยเฉพาะคำถามที่ขึ้นต้นว่า "แล้วถ้าใน Go มันทำแบบนี้ ใน TypeScript ทำยังไง" — คำถามแบบนั้นดีที่สุดสำหรับบทนี้ เพราะสิ่งที่ยากไม่ใช่ syntax ใหม่ แต่คือการเลิกคาดหวังพฤติกรรมเดิม