🧰 ของที่ใช้ตรวจในบทนี้
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 |
บวกกันได้หมด เพราะเป็นชนิดเดียวกัน | เขียนง่ายขึ้นจริง · แต่เสียตัวช่วยเตือนตอนที่คุณเผลอเอาจำนวนคนไปหารแล้วได้เศษ |
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 ตอนดึก ๆ ได้จริง
ใน 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 — ทำให้
tscฟ้องให้ได้ 3 แบบ
เขียนไฟล์lab.tsที่มีความผิดสามอย่างพร้อมกัน: ประกาศตัวแปรเป็นint· ใส่ string ลงในArray<number>· และอ่านช่องที่ object ไม่มี
รันtsc --noEmit --strict lab.tsแล้วจดเลข TS ทั้งสามตัวลงกระดาษ พร้อมบอกว่าเลขไหนคู่กับความผิดข้อไหน -
รอบที่ 2 — เปิดฝาดูว่า type หายไปตรงไหน
แก้ไฟล์ให้ไม่มี error แล้วใส่typeที่ตั้งชื่อเองอย่างน้อยหนึ่งอัน กับ: string: numberอย่างน้อยอย่างละหนึ่งจุด
รันtsc --target es2022 --module nodenext --moduleResolution nodenext lab.tsแล้วcat lab.js
นับให้ได้ว่าไฟล์.tsมี type กี่จุด และไฟล์.jsเหลือกี่จุด -
รอบที่ 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 บอกได้ว่าต้องไปแก้ตรงไหน เร็วกว่าเอาเลขไปค้นกูเกิล
อ้างอิง
- TypeScript Handbook — The Basics — อ่านหัวข้อ Static type-checking กับ Erased Types สองหัวข้อนี้พูดเรื่องเดียวกับหัวข้อ 2 ของบทนี้ และมีประโยคที่ว่า type ถูกลบทิ้งก่อนรันเขียนไว้ตรง ๆ
- TypeScript Handbook — Everyday Types —
อ่านตั้งแต่ The primitives ถึง Optional Properties
ครอบคลุม
string·number·boolean· array · object type ทั้งหมดของบทนี้ พร้อมประโยคที่ยืนยันว่าไม่มีintและfloat - TypeScript Handbook — Type Compatibility — อ่านย่อหน้าแรกอย่างเดียวก็พอ มันบอกว่า TypeScript ใช้ structural typing และเทียบให้ดูกับภาษาที่ใช้ nominal typing — ตรงกับที่หัวข้อ 5 พิสูจน์ให้ดู
- TypeScript Playground (สไลด์หน้า 8) —
วางโค้ดฝั่งซ้าย แล้วกดแท็บ
.JSฝั่งขวา จะเห็น type หายไปแบบเรียลไทม์โดยไม่ต้องลงtscในเครื่อง เหมาะกับทดลองเร็ว ๆ - MDN —
Number— อ่านย่อหน้า double-precision 64-bit binary format คือคำอธิบายว่าทำไม0.1 + 0.2ถึงไม่เท่ากับ0.3และทำไมถึงไม่มีintให้เลือก
ติดตรงไหนถามได้เลยครับ โดยเฉพาะคำถามที่ขึ้นต้นว่า "แล้วถ้าใน Go มันทำแบบนี้ ใน TypeScript ทำยังไง" — คำถามแบบนั้นดีที่สุดสำหรับบทนี้ เพราะสิ่งที่ยากไม่ใช่ syntax ใหม่ แต่คือการเลิกคาดหวังพฤติกรรมเดิม