Cheat sheet · Frontend · Functional Programming with TypeScript

TypeScript — type · interface · function · map/filter/reduce แผ่นเดียวจบ

คู่กับ บทเรียน 63–68 · ทุกผลลัพธ์ในหน้านี้มาจากการรันจริงด้วย tsc 7.0.2 และ Node v24.14.0

หน้านี้ไม่ได้เขียนไว้ให้อ่านรวดเดียว — เขียนไว้ให้เปิดตอนมือติดคีย์บอร์ด
ถ้ากำลังรีบ: หัวข้อ 11 (ตารางแปล error ของ tsc) · หัวข้อ 13 (ตารางกับดักรวมทั้งกลุ่ม) และหัวข้อ 10 (ใครแตะของเดิมบ้าง) คือสามอันที่จะได้ใช้บ่อยที่สุด
ถ้าเพิ่งมาจาก Go ให้เริ่มที่หัวข้อ 12 ก่อน — สามจุดที่ชนกับความเคยชินเดิมอยู่ตรงนั้น

🧰 ตรวจด้วยอะไร เวอร์ชันไหน — ทุกบรรทัดผลรันในหน้านี้มาจาก

tsc      Version 7.0.2      # จาก tsc -v
node     v24.14.0           # จาก node --version
go       go1.26.5 darwin/arm64   # ใช้เฉพาะหัวข้อ 12 ที่เทียบกับ Go

ทุก error code และทุกข้อความในหน้านี้ ก็อปมาจากผลรันจริงในบทเรียน 63–68 ไม่ได้พิมพ์จากความจำ · ถ้าเวอร์ชัน tsc ของคุณไม่ตรง ข้อความอาจต่างกันได้จริง แต่เลข TSxxxx มักเหมือนเดิม — ให้เชื่อผลรันบนเครื่องคุณ ไม่ใช่ตัวหนังสือในหน้านี้

สารบัญ

  1. คำสั่งที่ใช้จริงทุกวัน
  2. type พื้นฐาน + วิธีเขียน
  3. interface vs type
  4. any vs unknown
  5. as (type assertion)
  6. ใส่ type ให้ฟังก์ชัน
  7. method แปลงข้อมูล 6 ตัว
  8. reduce — สามกับดักที่ต้องรู้
  9. slice vs splice
  10. ใครแตะของเดิมบ้าง
  11. ตารางแปล error ของ tsc
  12. เทียบกับ Go สั้น ๆ
  13. ตารางกับดักรวมทั้งกลุ่ม
  14. อ้างอิง

1 · คำสั่งที่ใช้จริงทุกวัน

อยากทำอะไรพิมพ์หมายเหตุ
ตรวจ type อย่างเดียวtsc --noEmit --strict ไฟล์.tsไม่สร้างไฟล์ .js · ใช้ตัวนี้เป็นหลัก
คอมไพล์แล้วรันtsc --target es2022 --module nodenext --moduleResolution nodenext ไฟล์.ts && node ไฟล์.jsต้องมี package.json ที่มี { "type": "module" }
ดูเวอร์ชันtsc -v · node --versionข้อความ error ของ tsc 5 กับ 7 ไม่เหมือนกันบางอัน
ดูว่าผ่านไหมแบบไม่ต้องอ่านecho $?0 = ผ่าน · 1/2 = ไม่ผ่าน
⚠ tsc ฟ้อง error แล้วก็ยังสร้างไฟล์ .js ให้อยู่ดี
$ 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 ไม่ผ่านแปลว่าไม่มีไฟล์ให้รัน · ในโปรเจกต์จริงต้องให้ CI สั่ง tsc --noEmit เป็นด่าน แล้วดู exit code เอง (หรือเปิด noEmitOnError)

2 · type พื้นฐาน + วิธีเขียน

กลุ่มเขียนยังไงที่ต้องรู้
primitive const message: string = "Hello World";
const numeric: number = 1000;
const isEnabled: boolean = true;
มีสามตัวนี้ · ไม่มี int ไม่มี float — ทุกตัวเลขคือทศนิยม 64 บิตแบบเดียวกันหมด
array Array<number> หรือ number[] สองแบบนี้เหมือนกันเป๊ะ ต่างแค่รูปแบบการพิมพ์ · number[] เจอบ่อยกว่าในโค้ดจริง
object type { name: string; score: number } คั่นช่องด้วย เซมิโคลอน ตอนบรรยาย type แต่คั่นด้วย จุลภาค ตอนใส่ค่าจริง
optional property { name: string; note?: string } ไม่ใส่ค่า อ่านออกมาได้ undefined ไม่ใช่สตริงว่าง (ต่างจาก zero value ของ Go)
union type StudentId = number | string;
type AddressLabel = "home" | "work" | "other";
“เป็นอย่างใดอย่างหนึ่ง” · ใช้กับรายชื่อค่าตายตัวได้ด้วย · ต้องใช้ type — interface ทำไม่ได้
literal type status: "delivered" | "pending" | "cancelled" เขียนแบบนี้แทน string แล้วพิมพ์ผิดจะโดนจับทันที (ดู หัวข้อ 13)
tuple type PairTuple = [number, boolean, string]; array ที่ล็อกทั้งจำนวนช่องและชนิดของแต่ละช่อง · ต้องใช้ type
index signature Record<string, number> “object ที่ key เป็น string อะไรก็ได้ value เป็น number” · อ่าน key ที่ไม่มีจริงได้ undefined โดย tsc ไม่ทัก
void function logIt(msg: string): void “ฟังก์ชันนี้ไม่คายอะไรออกมา” · เขียน return ค่า เข้าไปจะได้ TS2322
$ 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 เป็นของ WebGL ไม่ใช่ชนิดตัวเลขของภาษา · ข้อความ “Did you mean…” เดาจากการสะกดที่คล้ายกัน ไม่ใช่จากความหมาย ให้เชื่อบรรทัด TS2304 ที่บอกว่า “ไม่มีชื่อนี้”

⚠ type หายไปหมดตอนรัน (type erasure)
// ไฟล์ .ts ที่เขียน
const message: string = "Hello World";
type Score = { name: string; score: number };
function total(list: Array<number>): number {

// ไฟล์ .js ที่ได้
const message = "Hello World";
(บรรทัด type Score หายไปทั้งบรรทัด)
function total(list) {

ผลที่ตามมา: เขียน instanceof Score ไม่ได้ — error TS2693: 'Score' only refers to a type, but is being used as a value here.
และ typeof johnDoeScore ตอนรันได้คำว่า object เฉย ๆ ไม่ใช่ Score

⚠ excess property check — ก้อนที่พิมพ์สดตรงนั้นถูกตรวจเข้มกว่า
type Score = { name: string; score: number };

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

// ท่าที่ 2 — พักไว้ในตัวแปรก่อน → ผ่านเงียบ ๆ
const box = { name: "Josie Watson", score: 50, grade: "A" };
const viaVariable: Score = box;
10-excess.ts(4,54): error TS2353: Object literal may only specify known properties,
and 'grade' does not exist in type 'Score'.

ผลกระทบจริง: ข้อมูลจาก API ที่มีช่องเกินมา จะไม่มีใครฟ้อง เพราะไม่ใช่ก้อนที่คุณพิมพ์เอง — อย่าคาดหวังว่า TypeScript จะกรองช่องแปลกปลอมออกให้

3 · interface vs type — อะไรทำได้/ไม่ได้ ฝั่งไหน

สิ่งที่อยากทำinterfacetypeผลรันที่ยืนยัน
บรรยายรูปร่าง object✅ ได้✅ ได้EXIT=0 ทั้งคู่
ตั้งชื่อให้ union (number | string)❌ ไม่ได้✅ ได้TS1005 / TS2312 ฝั่ง interface
ตั้งชื่อให้ tuple❌ ไม่ได้✅ ได้เหตุผลเดียวกับ union
ต่อยอดจากของเดิม✅ extends✅ & (intersection)EXIT=0 ทั้งคู่ และสลับใช้แทนกันได้
ประกาศชื่อเดิมซ้ำแล้วรวมกันเอง✅ ได้ (ระวัง)❌ ไม่ได้TS2300 ฝั่ง type · ฟ้องสองบรรทัด

interface — ประกาศซ้ำได้ (declaration merging)

interface Window2 { title: string; }
interface Window2 { ts: number; }

const w: Window2 = { title: "หน้าแรก", ts: 1 };

# ไม่มี output · EXIT=0

type — ประกาศซ้ำไม่ได้

type Window3 = { title: string; };
type Window3 = { ts: number; };
(1,6): error TS2300: Duplicate identifier 'Window3'.
(4,6): error TS2300: Duplicate identifier 'Window3'.
EXIT=1
$ # interface ที่ชื่อซ้ำถูกรวมเป็นก้อนเดียว → ต้องใส่ครบทั้งสองช่อง
a4-merge-miss.ts(8,7): error TS2741: Property 'ts' is missing in type
'{ title: string; }' but required in type 'Config'.
$ # interface ทำ union ไม่ได้ — สอง error นี้คือหลักฐาน
error TS1005: '{' expected.                    ← interface IStudentId = number | string;

e3-iface-extends-union.ts(3,30): error TS2312: An interface can only extend an
object type or intersection of object types with statically known members.
⚠ “เปิดเพิ่ม property ทีหลังได้” ไม่ใช่ฟีเจอร์ที่มีไว้ใช้ทุกวัน

declaration merging สร้างมาเพื่อต่อเติม type ของ library ที่คุณไม่ได้เขียนเอง (เช่นแปะค่าเพิ่มบน Window ของเบราว์เซอร์)
ใช้ในโค้ดของตัวเองเมื่อไหร่ = คุณเพิ่มช่องบังคับให้ไฟล์ของคนอื่นได้ โดยไม่ได้แตะไฟล์เขาเลย โค้ดที่เคยผ่านเมื่อวาน วันนี้พังโดยที่ไฟล์นั้นไม่ได้ถูกแก้

กฎง่าย ๆ ที่ใช้ได้ตั้งแต่วันนี้
· เป็น object ธรรมดา → ใช้อะไรก็ได้ แต่ให้ทั้งทีมใช้อันเดียวกัน
· ไม่ใช่ object (union · tuple · ชื่อเล่นของ string) → ต้องใช้ type ไม่มีทางเลือก
· กำลังจะต่อเติม type ของ library → ต้องใช้ interface ไม่มีทางเลือกเหมือนกัน
· นอกจากนั้น อย่าเสียเวลาเถียงกัน — ความต่างที่เหลือแทบไม่กระทบงานประจำวัน

4 · any vs unknown — อันไหนปล่อยผ่านอะไร

ไฟล์สองไฟล์ที่เหมือนกันทุกตัวอักษร ต่างแค่คำเดียว (const data: any กับ const data: unknown) แล้วสั่ง tsc --noEmit --strict:

บรรทัดในไฟล์ตั้งใจทำอะไรanyunknown
data.nameอ่านช่องที่มีอยู่จริงผ่านTS18046
data.profile.emailอ่านช่องที่ไม่มีผ่านTS18046
data.toUpperCase()เรียก method ของ string ใส่ objectผ่านTS18046
data()เรียก object เหมือนเป็นฟังก์ชันผ่านTS18046
const n: number = dataยัด object ใส่ตัวแปรเลขผ่านTS2322
const s: string = dataยัด object ใส่ตัวแปรข้อความผ่านTS2322
รวม0 error · EXIT=06 error · EXIT=1
$ # ฝั่ง unknown
b2-unknown.ts(3,13): error TS18046: 'data' is of type 'unknown'.
b2-unknown.ts(4,13): error TS18046: 'data' is of type 'unknown'.
b2-unknown.ts(5,13): error TS18046: 'data' is of type 'unknown'.
b2-unknown.ts(6,1):  error TS18046: 'data' is of type 'unknown'.
b2-unknown.ts(7,7):  error TS2322: Type 'unknown' is not assignable to type 'number'.
b2-unknown.ts(8,7):  error TS2322: Type 'unknown' is not assignable to type 'string'.

หกต่อศูนย์ · สังเกตว่ามันไม่ใช่ “any ตรวจน้อยลง” — มันไม่ตรวจอะไรเลยสักบรรทัด แม้แต่บรรทัดที่เรียก object เหมือนเป็นฟังก์ชัน ซึ่งผิดชัด ๆ ด้วยตาเปล่า

⚠ any แพร่กระจาย — ฟังก์ชันตัวเดียวทำให้ตัวแปรปลายทางเชื่อถือไม่ได้
function loadConfig(): any {
	return { retry: "3" };        // ← เป็น string นะ ไม่ใช่เลข
}
const cfg = loadConfig();
const retry: number = cfg.retry;   // เขียนไว้ชัดว่าเป็น number
# npx tsc --noEmit --strict e1-spread.ts
EXIT_TSC=0

# node e1-spread.js
retry = 3
retry + 1 = 31                     ← ไม่ใช่ 4 เพราะค่าจริงคือ "3" แล้ว "3" + 1 คือการต่อข้อความ
TypeError: retry.toFixed is not a function

เปลี่ยน loadConfig(): any เป็น loadConfig(): unknown อย่างเดียว แล้วรันซ้ำ ได้ทันที: e2-spread-unknown.ts(6,23): error TS18046: 'cfg' is of type 'unknown'.
ผลที่ any ให้ไม่ใช่ error สวย ๆ แต่เป็นค่าที่ดูเหมือนใช้ได้ อย่าง 31 ที่ไหลต่อไปได้อีกไกล

ใช้ unknown ยังไงให้ทำงานได้จริง — narrow ก่อนใช้
const data: unknown = JSON.parse('{"name":"Ann"}');

if (typeof data === "object" && data !== null && "name" in data) {
	console.log("ผ่านด่านแล้ว name =", data.name);
} else {
	console.log("ข้อมูลไม่ตรงรูปที่คาดไว้ ไม่แตะมัน");
}
EXIT_TSC=0
ผ่านด่านแล้ว name = Ann
ข้อมูลไม่ตรงรูปที่คาดไว้ ไม่แตะมัน       # เคส JSON.parse('123') ที่ได้เลข ไม่ใช่ object
EXIT_NODE=0

data.name ข้างใน if ไม่ error แล้ว — compiler ตามอ่านเงื่อนไขแล้วยอมรับว่าคุณพิสูจน์มาแล้ว · ราคาที่จ่ายคือ if สามเงื่อนไข ซึ่งเป็นงานที่ต้องทำอยู่แล้วทุกครั้งที่รับข้อมูลจากข้างนอก — any ไม่ได้ทำให้งานนี้หายไป มันแค่ทำให้คุณลืมว่ามีงานนี้อยู่

5 · as (type assertion) — ทำอะไรได้ ทำอะไรไม่ได้

as ทำอะไรas ไม่ทำอะไร
สั่ง compiler ว่า “ค่านี้เป็น type นี้ ไม่ต้องตรวจ” ไม่แปลงค่า — ไม่มีการ convert เกิดขึ้นเลยแม้แต่นิดเดียว
assert ไป type ที่กว้างกว่าหรือแคบกว่าของเดิม ไม่สร้างช่องใหม่ — ช่องที่ขาดยังขาดอยู่ตอนรัน
หายไปตอนคอมไพล์ เหมือนไม่เคยมี ไม่เช็คตอนรัน — ไม่มี code ตรวจอะไรถูกใส่เข้าไปใน .js เลย
ปิดปาก compiler ได้เงียบสนิท (ไม่มีแม้แต่ warning สีเหลือง) assert ข้ามโลกไม่ได้ — n as string เมื่อ n เป็นเลข → TS2352

ใส่ as — compiler เงียบ

const initGame = {
	round: "ROUND 1",
	score: 0,
} as TwentyFourGame;

// EXIT=0 · พังตอน runtime

ไม่ใส่ as — โดนจับทันที

const initGame: TwentyFourGame = {
	round: "ROUND 1",
	score: 0,
};

// EXIT=1 · TS2322 พร้อมชื่อช่องที่ขาด
a7-noassert.ts(11,7): error TS2322: Type '{ round: string; score: number; }' is not
assignable to type 'TwentyFourGame'.
  Type '{ round: string; score: number; }' is missing the following properties
  from type '{ streak: number; hasTimer: boolean; }': streak, hasTimer
⚠ ฝั่งที่ใส่ as — มันแย่ลงทีละขั้นแบบพอดี ๆ
# npx tsc --noEmit --strict a6-assert.ts
EXIT_TSC=0                      ← ผ่านฉลุย ไม่มีคำเตือนแม้แต่ตัวเดียว

# node a6-assert.js
compile ผ่านแล้ว initGame = { round: 'ROUND 1', score: 0 }   ← ไม่มี streak อยู่จริง ๆ
streak = undefined                                            ← undefined เงียบ ๆ
streak + 1 = NaN                                              ← ยังไม่ crash · ค่าเน่าไหลต่อได้อีกไกล
TypeError: Cannot read properties of undefined (reading 'toFixed')  ← ถึงจะพัง

ที่ที่มันระเบิด กับที่ที่มันเริ่มผิด อยู่คนละที่กัน — นี่คือเหตุผลที่ bug จาก as หายาก

⚠ as unknown as X = double assertion = ธงแดง
error TS2352: Conversion of type 'number' to type 'string' may be a mistake
because neither type sufficiently overlaps with the other. If this was intentional,
convert the expression to 'unknown' first.

อ่านประโยคสุดท้ายของ error ให้ดี — มันสอนวิธีแหกกฎให้เอง · ทำตามจริง ๆ ก็ผ่าน: const s = n as unknown as string; → EXIT=0
แล้วรันจริง: TypeError: s.toUpperCase is not a function
สอง as ต่อกันคือสัญญาณว่ากำลังฝืนของแรง ๆ — as ที่ผ่านได้ ไม่ได้แปลว่าถูก แปลว่าคุณรับผิดชอบเองตั้งแต่วินาทีที่พิมพ์คำนั้น

ใช้คำให้ถูกตอนคุยงาน — as Type คือ assertion = “สั่งไม่ต้องตรวจ” · : Type คือ annotation = “ขอให้ตรวจ” · หน้าตาใกล้กันมากแต่คนละเรื่องสิ้นเชิง

6 · ใส่ type ให้ฟังก์ชัน

ทรงที่ 1 — function declaration

function sum(num1: number, num2: number): number {
	return num1 + num2;
}
//        ↑ ชนิดของ    ↑ ชนิดของ     ↑ ชนิดของของที่คายออกมา

ทรงที่ 2 — arrow function

const sum = (num1: number, num2: number): number => {
	return num1 + num2;
};

ความสามารถเท่ากันเป๊ะ ต่างแค่รูปประโยค · สองก้อนนี้อยู่ไฟล์เดียวกันไม่ได้ เพราะชื่อ sum ซ้ำ → error TS2300: Duplicate identifier 'sum'.

เขียนแบบชนิดจริงข้างในฟังก์ชันต้องเช็คก่อนใช้ไหมวางตรงไหนได้
num2: number (บังคับ)numberไม่ต้องก่อนพวก ? ทั้งหมด
num2?: number (optional)number | undefinedต้อง ไม่งั้น TS18048หลังช่องบังคับ · ก่อน rest
num2: number = 0 (default)numberไม่ต้องที่ไหนก็ได้ · ถ้าอยู่กลางต้องส่ง undefined มาข้าม
...nums: number[] (rest)number[] (ว่างได้ แต่ไม่เป็น undefined)ไม่ต้องช่องสุดท้ายเท่านั้น
): number { (return type)——หลังวงเล็บปิด · เขียนกำกับไว้ดีกว่าปล่อยให้เดา (ดูกล่องล่าง)
): void {——“ไม่คายค่า” · ใส่ return ค่า เข้าไปได้ TS2322
$ # ลำดับ parameter ผิด — สองข้อนี้เป็นเรื่องเดียวกัน คือ argument ระบุด้วยตำแหน่ง
order.ts(2,31): error TS1016: A required parameter cannot follow an optional parameter.
order.ts(6,16): error TS1014: A rest parameter must be last in a parameter list.

$ # ใส่ทั้ง ? และ = ที่ช่องเดียวกัน
both.ts(1,28): error TS1015: Parameter cannot have question mark and initializer.
ของแถมที่ tsc จับให้ฟรี ๆ ตอนคนอื่นเรียกใช้
callsite.ts(6,1):  error TS2554: Expected 2 arguments, but got 1.     ← sum(2)
callsite.ts(7,11): error TS2554: Expected 2 arguments, but got 3.     ← sum(2, 3, 4)
callsite.ts(8,8):  error TS2345: Argument of type 'string' is not assignable
                                 to parameter of type 'number'.       ← sum(2, "3")

noparam.ts(2,16): error TS7006: Parameter 'name' implicitly has an 'any' type.
voidret.ts(4,3):  error TS2322: Type 'number' is not assignable to type 'void'.

ทั้งสามอันบนใน JavaScript ล้วน ๆ รันผ่านหมด ไม่มีใครทัก — sum(2) จะได้ NaN ส่วน sum(2, "3") จะได้สตริง "23"

⚠ ? เปลี่ยนชนิด ไม่ได้แค่ทำให้ “ไม่ต้องส่งก็ได้”
const sum = (num1: number = 0, num2?: number): number => {
	return num1 + num2
}
slide28a.ts(3,17): error TS18048: 'num2' is possibly 'undefined'.

ดื้อยัด as any ให้ผ่านแล้วรันจริง จะได้ 5 แล้วก็ NaN — ไม่พัง เพราะ 2 + undefined ใน JavaScript ไม่ใช่ error มันคือ NaN ที่เดินทางต่อไปได้อีกสิบไฟล์

ทางแก้ที่ถูกคือ เขียน if ให้ compiler เห็นด้วยตาตัวเอง (narrowing) ไม่ใช่ยัด as: if (num2 === undefined) { return num1; } แล้วบรรทัดล่างบวกได้เลย

เขียน return type กำกับไว้ ย้ายที่เกิดเหตุให้ตรงกับที่ผิดจริง
=== A (ไม่เขียน return type) ===
infer-a.ts(6,7): error TS2322: Type 'string' is not assignable to type 'number'.
                              ← บรรทัด 6 คือบรรทัดที่ "คนเรียกใช้"

=== B (เขียน return type) ===
infer-b.ts(3,3): error TS2322: Type 'string' is not assignable to type 'number'.
                              ← บรรทัด 3 คือบรรทัด return ที่พิมพ์ผิดจริง

เลข error เดียวกัน ข้อความเดียวกัน แต่คนละบรรทัด · ฟังก์ชันที่มีคนเรียก 20 ที่ใน 8 ไฟล์ → แบบ A ให้ error 20 อันในไฟล์ที่ไม่ได้ผิด · แบบ B ให้ error อันเดียวตรงจุดที่พิมพ์ผิดพอดี

arrow vs function ปกติ — ต่างกันจริงที่ this
function ปกติ: this ถูกตัดสินตอนถูกเรียก · arrow function: ไม่มี this ของตัวเอง หยิบของที่ล้อมมันอยู่มาใช้
ผลรันจริงตอนวน forEach ในตัว object เดียวกัน: arrow → this.label = ตะกร้า · normal → this = undefined
(ได้ undefined เพราะไฟล์ .js ที่ tsc คอมไพล์ออกมามี "use strict" บรรทัดแรก · ไม่มีบรรทัดนั้นจะได้ globalThis แทน)
กฎที่ใช้ได้จริง: callback ใช้ arrow · method ของ object ใช้ทรงปกติ

7 · method แปลงข้อมูล 6 ตัว

methodคืนอะไรแก้ของเดิมไหมใช้ตอนไหน
arr.join(", ") string เสมอ ไม่ว่าข้างในจะเป็นอะไร ไม่แก้ array → string เพื่อเอาไปแสดงบนหน้าจอ · ตัวในวงเล็บคือตัวคั่นระหว่างชิ้น
str.split(", ") string[] เสมอ ไม่เคยคืน null ไม่แก้ (string แก้ไม่ได้อยู่แล้ว) string → array เพื่อเอาไปวนลูป นับ กรอง
x.slice(2, 6) ชนิดเดิม — ใส่ string ได้ string ใส่ array ได้ array ไม่แก้ ตัดเอาบางช่วง · slice(เริ่ม, ก่อนถึง) · เลขติดลบ = นับจากท้าย
arr.map(fn) array ใหม่ ยาวเท่าเดิมเสมอ · type เปลี่ยนได้ ไม่แก้ แปลงทุกชิ้นเป็นของใหม่ เช่นดึงเฉพาะช่องที่จะเอาไปแสดง
arr.filter(fn) array ใหม่ สั้นลงหรือเท่าเดิม · type ไม่เปลี่ยน ไม่แก้ คัดเฉพาะชิ้นที่ผ่านเงื่อนไข · callback ตอบแค่จริง/เท็จ
arr.reduce(fn, init) ค่าเดียว · type มาจาก init ไม่ใช่จาก callback ไม่แก้ ยุบทั้งกองให้เหลือค่าเดียว — จะเป็นเลข object หรืออะไรก็ได้

ผลรันจริงของ join · split ที่ต้องจำ

$ node a-join.js
1) Alice, Bob        // ["Alice","Bob"].join(", ")
2) string            // typeof — คืน string เสมอ
3) Alice,Bob         // join() เปล่า ๆ = คอมมา ไม่ใช่ "ไม่มีตัวคั่น"
4) AliceBob          // join("") ต่างหากคือไม่มีอะไรคั่น
5) a---b             // ["a",null,undefined,"b"].join("-") — null/undefined กลายเป็นข้อความว่าง
6)                   // [].join(", ") ได้ข้อความว่าง ไม่ใช่ error
8) 2026-8-27         // [2026,8,27].join("-") — array ของเลขก็ join ได้
$ node b-split.js
3)  [ 'Hello', 'World' ]                       // "Hello World".split(" ")
6)  [ 'Hello World' ]                          // split(",") หา delimiter ไม่เจอ → ยาว 1 ไม่ใช่ว่าง!
7)  [ 'Alice', ' Bob', ' Carol' ]              // split(",")  ← เคาะติดหน้าชื่อ
8)  [ 'Alice', 'Bob', 'Carol' ]                // split(", ") ← ต่างกันแค่เคาะเดียว
9)  [ '', 'a', 'b', '' ]                       // ",a,b,".split(",") — ได้ชิ้นว่างหัวท้าย
10) [ '' ]                                     // "".split(",")  → ยาว 1
11) []                                         // "".split("")   → ยาว 0
12) [ 'a', 'b' ]                               // "a-b-c-d".split("-", 2) — ตัวที่สองคือ limit
⚠ กับดักคนไทย: split("") ไม่ได้แบ่งตามที่ตาเห็น
13) [ 'ก', 'ิ', 'น' ]     // "กิน".split("") → คนเห็น 2 ตัว แต่ได้ 3 (สระอิแยกต่างหาก)
14) 8                     // "👨‍👩‍👧".split("").length → emoji ตัวเดียว ได้ 8
15) 5                     // [..."👨‍👩‍👧"].length → ยังไม่ใช่ 1 อยู่ดี

ห้ามใช้ split("") นับความยาวหรือกลับด้านข้อความ ถ้าข้อความอาจมีภาษาไทยหรือ emoji — เทสภาษาอังกฤษจับไม่ได้ · ถ้าต้องหั่นให้ตรงกับที่ตาเห็นต้องใช้ Intl.Segmenter

$ # join() เว้นว่างได้ แต่ split() เว้นว่างไม่ได้ — tsc จับให้
b2-split-noarg.ts(7,26): error TS2554: Expected 1-2 arguments, but got 0.

$ # ลืมว่า join คืน string ไม่ใช่ array
g-typeerr.ts(3,7): error TS2322: Type 'string' is not assignable to type 'string[]'.

กติกาการนับของ slice

ตำแหน่ง:  0   1   2   3   4   5     6   7   8   9   10
ตัวอักษร: H   e   l   l   o  (เคาะ)  W   o   r   l   d
                  ↑                   ↑
              เริ่มที่ 2          จบก่อนถึง 6

"Hello World".slice(2, 6)   → "llo "      ความยาว = เลขหลัง − เลขหน้า = 4
"Hello World".slice(2)      → "llo World"
"Hello World".slice(-5)     → "World"     เอา 5 ตัวสุดท้าย
"Hello World".slice(-5, -1) → "Worl"      5 ตัวสุดท้าย ตัดตัวท้ายทิ้ง
"Hello World".slice(6, 2)   → ""          เลขหน้ามากกว่าเลขหลัง = ว่าง ไม่กลับด้านให้
"Hello World".slice(2, 100) → ยาว 9       เลขหลังเกินขอบ หยุดที่ปลายให้เอง

สูตรที่ใช้บ่อยที่สุด: slice(0, -1) = ตัดตัวท้ายทิ้งหนึ่งตัว · slice(-1) = เอาแต่ตัวท้าย — สองอันนี้ตรงข้ามกัน อย่าสลับ

ต่อกันใช้จริง — split → map → filter → slice → join

const raw: string = "Alice, Bob ,Carol,  Dave";

const cleaned: string[] = raw.split(",").map((n) => n.trim());
const firstThree: string[] = cleaned.slice(0, 3);
const label: string = firstThree.join(" · ");
$ node f-pipeline.js
1) [ 'Alice', ' Bob ', 'Carol', '  Dave' ]     ← split เฉย ๆ ยังมีเคาะติด
2) [ 'Alice', 'Bob', 'Carol', 'Dave' ]         ← หลัง .trim()
3) [ 'Alice', 'Bob', 'Carol' ]
4) Alice · Bob · Carol
5) ของเดิมยังอยู่: Alice, Bob ,Carol,  Dave    ← ผ่านมาสี่ขั้น raw ยังเหมือนเดิมเป๊ะ
chain แทน for loop
const totalPaid = orders
	.filter((o) => o.status === "paid")
	.map((o) => o.amount)
	.reduce((sum, amount) => sum + amount, 0);

อ่านจากบนลงล่างได้เป็นประโยค: คัดที่จ่ายแล้ว → เอาเฉพาะยอด → บวกรวม · ได้ 400 เท่ากับ for loop เป๊ะ และ orders ยังครบ 4 ชิ้น
แต่ chain ไม่ได้เร็วกว่า — for เดินรอบเดียว · chain 3 ชั้นเดิน 3 รอบ และสร้าง array กลางทาง 2 ก้อน · กับข้อมูลหลักร้อยหลักพันบนหน้าเว็บ วัดความต่างไม่ได้เลย

สถานการณ์เลือกเพราะ
แปลงทุกชิ้น · คัดบางชิ้น · รวมเป็นค่าเดียวmap/filter/reduceชื่อ method บอกเจตนาตั้งแต่คำแรก
ต้อง break ออกกลางคัน (เจอตัวแรกที่ใช่แล้วพอ)for · หรือ find / somemap/filter/reduce หยุดกลางทางไม่ได้ เดินจนจบเสมอ
เดินสอง array พร้อมกันด้วย index เดียวกันforเขียนตรงไปตรงมากว่า
callback ต้อง await ทีละชิ้นตามลำดับfor…ofmap กับ async จะได้ array ของ Promise ไม่ใช่ค่าจริง
ข้อมูลหลักแสนชิ้น และวัดแล้วว่าช้าจริงforเดินรอบเดียว ไม่สร้าง array กลางทาง — ต้องวัดก่อน ไม่ใช่เดา

ของที่มีเป็น object ไม่ใช่ array

Object.keys(config)     → [ 'host', 'port' ]
Object.values(config)   → [ 'localhost', 3000 ]
Object.entries(config)  → [ [ 'host', 'localhost' ], [ 'port', 3000 ] ]

// พอเป็น array แล้ว ทุกอย่างในหัวข้อนี้ใช้ได้ทันที
Object.entries(stock).filter(([, count]) => count > 0).map(([name]) => name)
// → [ 'apple', 'cherry' ]

8 · reduce — สามกับดักที่ต้องรู้

⚠ ① ไม่ใส่ค่าเริ่มต้น + array ว่าง = โปรแกรมตาย · และ tsc ไม่เตือนเลย
const prices: number[] = [];
const total = prices.reduce((sum, p) => sum + p);
$ tsc --noEmit --strict     # exit code 0 · ไม่มี error สักบรรทัด

$ node t1-reduce-empty.js
TypeError: Reduce of empty array with no initial value
    at Array.reduce (<anonymous>)
Node.js v24.14.0

แก้ง่ายมาก — ใส่ , 0 ต่อท้าย · ต้นทุนของการใส่คือตัวอักษร 3 ตัว ต้นทุนของการไม่ใส่คือหน้าเว็บพังในวันที่ผู้ใช้คนแรกเปิดหน้าตะกร้าที่ยังว่างเปล่า

⚠ ② type ของ reduce มาจากค่าเริ่มต้น ไม่ใช่จาก callback
const prices = [15, 3, 40];
const total = prices.reduce((sum, price) => sum + price, "");   // พิมพ์ "" แทน 0
$ tsc --noEmit --strict     # exit code 0 · ไม่มี error
$ node
15340 string                # console.log(total, typeof total)

ค่าเริ่มต้นเป็น string → tsc สรุปว่า sum เป็น string → sum + price กลายเป็นการต่อสตริง ซึ่งถูกกฎหมายทุกประการ · ไม่มีใครผิด ไม่มีใครบ่น

ผิดคนละทางถึงจะจับได้ แต่ error อ่านยาก:

t3c-init-num.ts(3,44): error TS2769: No overload matches this call.
  The last overload gave the following error.
    Type 'string' is not assignable to type 'number'.

error ชี้ไปที่ ตำแหน่ง callback (คอลัมน์ 44) ทั้งที่ตัวที่พิมพ์ผิดคือ 0 ท้ายบรรทัด · เห็น TS2769 บน reduce เมื่อไหร่ ให้ไปดูค่าเริ่มต้นก่อนเสมอ

⚠ ③ accumulator เป็น object แล้วลืมบอก type / ลืม ?? 0
t3b-empty-obj.ts(5,3): error TS7053: Element implicitly has an 'any' type because
  expression of type 'string' can't be used to index type '{}'.
  No index signature with a parameter of type 'string' was found on type '{}'.

ข้อความเดียวกันเป๊ะขึ้นมา 3 ก้อน เพราะบรรทัดนั้นเขียน acc[letter] ไว้ 3 ที่ — จำนวน error ไม่เท่ากับจำนวนจุดที่ต้องแก้ ให้ดูว่าข้อความซ้ำกันไหมก่อนตกใจ

ทางแก้คือบอก type ตรง ๆ: reduce<Record<string, number>>(…) · แล้วอย่าลืม ?? 0:

// ถูก
acc[e.role] = (acc[e.role] ?? 0) + e.dailyRate * e.workDays;
// → { developer: 92400, designer: 39600, manager: 55000 }

// ลบ ?? 0 ออก — tsc --strict ผ่านฉลุย exit=0 แต่รันจริงได้
acc[e.role] = acc[e.role] + e.dailyRate * e.workDays;
// → { developer: NaN, designer: NaN, manager: NaN }

ทำไม --strict ไม่จับ: Record<string, number> แปลว่า “key อะไรก็ได้ชี้ไปหา number” — ในสายตา tsc acc["ที่ไม่มีจริง"] ก็ยังเป็น number · ตัวที่จับได้จริงคือ flag --noUncheckedIndexedAccess ซึ่งจะฟ้อง error TS2532: Object is possibly 'undefined'.

?? ไม่ใช่ || — ?? เปลี่ยนค่าให้เฉพาะตอนฝั่งซ้ายเป็น null/undefined · ส่วน || เปลี่ยนตอน 0 และ "" ด้วย · เรื่องเงินใช้ ?? เท่านั้น

9 · slice vs splice — ต่างกันตัว p ตัวเดียว

const a: string[] = ["Alice", "Bob", "Carol", "Dave"];
const x = a.slice(1, 3);

const b: string[] = ["Alice", "Bob", "Carol", "Dave"];
const y = b.splice(1, 3);
$ node p2.js
x = [ 'Bob', 'Carol' ]           | a เหลือ 4 ตัว
y = [ 'Bob', 'Carol', 'Dave' ]   | b เหลือ 1 ตัว
slicesplice
ของเดิมไม่แตะโดนแก้
คืนอะไรส่วนที่ตัดมา (ก้อนใหม่)ส่วนที่ควักทิ้ง ไม่ใช่ส่วนที่เหลือ
เลขตัวที่สองตำแหน่งที่จะหยุดก่อนถึงจำนวนชิ้นที่จะลบ
เลขติดลบได้ทั้งสองตัว (นับจากท้าย)ได้เฉพาะตัวแรก · ตัวที่สองติดลบ = ลบ 0 ชิ้น
ใช้กับ stringได้ไม่มี — string แก้ไม่ได้ทั้งภาษา
แทรกของใหม่ไม่ได้ได้ — e.splice(2, 0, "Carol")
$ # tsc จับให้ พร้อมเดาให้ด้วยว่าน่าจะหมายถึงอะไร
d2-string-splice.ts(3,27): error TS2551: Property 'splice' does not exist on type 'string'. Did you mean 'slice'?
⚠ splice ที่แทรกของใหม่ คืน array ว่าง
const removed = e.splice(2, 0, "Carol");   // ที่ตำแหน่ง 2 ลบ 0 ชิ้น แล้วยัด "Carol" เข้าไป
7) splice คืน: []
8) ของเดิม  : [ 'Alice', 'Bob', 'Carol', 'Dave' ]

ผลของงานอยู่ที่ตัวแปรเดิม ไม่ได้อยู่ที่ค่าที่คืนมา · ใครเขียน const newList = e.splice(2, 0, "Carol") จะได้ [] แล้วงงว่าของหายไปไหน

10 · ใครแตะของเดิมบ้าง

methodทำอะไรแตะของเดิมไหม
join(", ")array → string เชื่อมด้วยตัวคั่นไม่แตะ
split("")string → arrayไม่แตะ
slice(2, 6)ตัดช่วงออกมา (index 2 ถึงก่อน 6)ไม่แตะ
map(fn)แปลงทุกชิ้นเป็นของใหม่ไม่แตะ
filter(fn)คัดเฉพาะที่ผ่านเงื่อนไขไม่แตะ
reduce(fn, init)ยุบทั้งกองเหลือค่าเดียวไม่แตะ
sort(cmp)เรียงลำดับ⚠ แตะ! เรียงของเดิม แล้วคืนก้อนเดิมกลับมา
splice(…)ควักออก / แทรกเข้า⚠ แตะ!
reverse() · push()กลับด้าน / เพิ่มท้าย⚠ แตะ!
⚠ sort คือกับดักใหญ่ที่สุดของกลุ่มนี้ — และไม่มีใครเตือนเลย
const best = movies.sort((a, b) => b.rating - a.rating).slice(0, 3);
console.log(movies.map((m) => m.title));   // ← ยังไม่ได้แตะ movies เลยสักที
[ 'Inception', 'Interstellar', 'Parasite' ]
[
  'Inception',
  'Interstellar',
  'Parasite',
  'John Wick',
  'Avengers',
  'The Room'          ← movies ถูกเรียงใหม่ไปแล้ว ทั้งที่โค้ดไม่มีตรงไหนสั่งให้เปลี่ยน
]

tsc --strict เงียบสนิท ไม่ error ไม่ warn เพราะในสายตา type system คุณแค่เรียก method ที่ถูกต้องบนตัวแปรที่ถูกต้อง · อาการจะโผล่ที่อีกหน้าจอหนึ่งที่ใช้ movies อยู่ แล้วคนก็จะไปไล่บั๊กผิดที่

ทางแก้ — ก็อปกองใหม่ก่อนแล้วค่อยเรียง
function topRated(list: Movie[], n: number): Movie[] {
	return [...list]                          // ← ก็อปกองใหม่ก่อน
		.sort((a, b) => b.rating - a.rating)   // b − a = มากไปน้อย
		.slice(0, n);
}
// → [ 'Inception 8.8', 'Interstellar 8.6', 'Parasite 8.5' ] · movies ลำดับเดิมครบ

มีอีกท่าที่สั้นกว่าคือ toSorted() ซึ่งคืน array ใหม่ให้เลย แต่มันมาใน ES2023 — บน --target es2022 จะได้ 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.

⚠ slice ก๊อป “ชั้นเดียว” — ข้างในเป็น object ยังแชร์กันอยู่
3) part    = [ { name: 'ALICE' }, { name: 'Bob' } ]
4) teamObj = [ { name: 'ALICE' }, { name: 'Bob' }, { name: 'Carol' } ]
5) เป็นก้อนเดียวกันไหม: true

s2[0] = "BOB" คือเปลี่ยนว่าช่องนั้นชี้ไปที่ไหน → ของเดิมไม่ขยับ · แต่ part[0].name = "ALICE" คือเข้าไปแก้ข้างในของที่ช่องนั้นชี้อยู่ → โดนของเดิมด้วย
สรุป: slice ทำให้ “กล่อง” แยกกัน แต่ “ของในกล่อง” ยังเป็นชิ้นเดียวกัน · ต้องการก๊อปทุกชั้นใช้ structuredClone()

11 · ตารางแปล error ของ tsc ที่เจอบ่อย

เลขข้อความจริงแปลว่าอะไร · แก้ยังไง
TS18048 'num2' is possibly 'undefined'.
'u.phone' is possibly 'undefined'.
ค่านั้นเป็น ชนิด | undefined เพราะมี ? ต่อท้าย · แก้: เขียน if เช็คก่อนใช้ (narrowing) หรือใช้ ?? ค่าตั้งต้น — อย่ายัด as เพราะจะได้ NaN เงียบ ๆ ตอนรัน
TS2552 Cannot find name 'numbers'. Did you mean 'Number'?
Cannot find name 'float'. Did you mean 'GLfloat'?
ไม่มีชื่อนี้อยู่จริง — ส่วนใหญ่คือพิมพ์ชื่อตัวแปรไม่ตรงกับที่ประกาศ · แก้: แก้ชื่อให้ตรง · ข้อความ “Did you mean…” เดาจากการสะกด ไม่ใช่ความหมาย อย่าเชื่อ
TS7006 Parameter 'name' implicitly has an 'any' type. ลืมใส่ type ให้ parameter (หรือ tsc เดาไม่ออกเพราะมี error อื่นอยู่ก่อนหน้า) · มาจากธง noImplicitAny ที่ --strict เปิดให้ · แก้: ใส่ type · ถ้ามาเป็นพวง ให้แก้อันบนสุดอันเดียวแล้วรันใหม่
TS2322 Type 'string' is not assignable to type 'number'.
Type '"office"' is not assignable to type 'AddressLabel'.
ยัดค่าผิดชนิดลงตัวแปร (คำว่า assignable เป็นตัวบอก) · แก้: แก้ค่าหรือแก้ type · ถ้าเป็น union แปลว่าค่าที่ใส่ไม่อยู่ในรายการ
TS2345 Argument of type 'string' is not assignable to parameter of type 'number'. ส่งค่าผิดชนิดเข้าฟังก์ชัน (คำว่า parameter เป็นตัวบอก) · push() ก็คือฟังก์ชัน จึงได้เลขนี้ ไม่ใช่ TS2322
TS2339 Property 'name' does not exist on type '{ firstName: string; lastName: string; role: string; }'. อ่านช่องที่ object นั้นไม่มี · แก้: เช็คชื่อช่องให้ตรง — ใน JavaScript ล้วน ๆ บรรทัดนี้ไม่ error แต่จะพิมพ์ undefined ออกมา
TS2352 Conversion of type 'number' to type 'string' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first. as ข้ามไป type ที่ไม่ทับกันเลย · ทางที่ error สอนเอง คือ as unknown as X ซึ่งผ่านจริง แต่พังตอนรัน — ถือเป็นธงแดง อย่าใช้เพราะเห็นในข้อความ
TS18046 'data' is of type 'unknown'.
'cfg' is of type 'unknown'.
เอา unknown ไปใช้โดยยังไม่พิสูจน์ · แก้: narrow ด้วย typeof / in / Array.isArray ก่อน — นี่คือ error ที่คุณอยากให้ขึ้น มันคือจุดที่ต้องตัดสินใจอยู่ดี
TS2741 Property 'y' is missing in type '{ x: number; }' but required in type 'Point'.
Property 'isDefault' is missing in type '…' but required in type 'ShippingAddress'.
ขาดช่องที่บังคับ · แก้: เติมช่องที่ขาด หรือทำช่องนั้นเป็น optional ด้วย ? ถ้ามันควรมีหรือไม่มีก็ได้จริง ๆ

เลขอื่นที่เจอในบท 63–68

เลขแปลว่า
TS2304Cannot find name 'int'. — ไม่มีชื่อนี้ในภาษา (คนละเรื่องกับ TS2322 ที่ “รู้จักทั้งคู่แต่ยัดใส่กันไม่ได้”)
TS2300Duplicate identifier 'Status'. — ชื่อซ้ำ · ฟ้องสองบรรทัดเพราะ compiler ไม่รู้ว่าอันไหนตัวจริง · เกิดกับ type แต่ไม่เกิดกับ interface
TS2353Object literal may only specify known properties, and 'grade' does not exist in type 'Score'. — excess property check ของก้อนที่พิมพ์สดตรงนั้น
TS2693'Score' only refers to a type, but is being used as a value here. — type ไม่มีตัวตนตอนรัน จะเอาไปใช้เป็นค่าไม่ได้
TS1005'{' expected. — เขียน interface X = … · parser คาดว่าจะเจอปีกกา
TS2312An interface can only extend an object type or intersection of object types with statically known members. — interface ต่อยอดจาก union ไม่ได้
TS1015Parameter cannot have question mark and initializer. — ใส่ทั้ง ? และ = ค่า ที่ช่องเดียวกัน เลือกอย่างเดียว
TS1016A required parameter cannot follow an optional parameter. — ช่องบังคับต้องอยู่ก่อนช่อง ?
TS1014A rest parameter must be last in a parameter list. — ... ต้องเป็นช่องสุดท้าย
TS2554Expected 2 arguments, but got 1. · Expected 1-2 arguments, but got 0. — จำนวน argument ไม่ตรง (อันหลังคือ split() ที่เว้นว่าง)
TS2551Property 'splice' does not exist on type 'string'. Did you mean 'slice'? — string ไม่มี splice
TS2683'this' implicitly has type 'any' because it does not have a type annotation. — function ปกติที่ส่งเข้า callback แล้วอ้าง this
TS2769No overload matches this call. — บน reduce แปลว่าค่าเริ่มต้นผิด type ให้ไปดูตัวสุดท้ายหลังจุลภาคก่อน
TS7053Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'. — accumulator เป็น {} เปล่า ๆ ต้องใส่ reduce<Record<…>>
TS2365Operator '+' cannot be applied to types 'Order' and 'number'. — reduce ที่ลืมค่าเริ่มต้น จึงหยิบสมาชิกตัวแรกมาเป็น accumulator ·
Operator '>' cannot be applied to types 'void' and 'number'. — เห็นคำว่า void ทั้งที่ไม่เคยพิมพ์เอง = ไปหาปีกกาที่ลืม return
TS2367This comparison appears to be unintentional because the types '"cancelled" | "delivered" | "pending"' and '"deliverd"' have no overlap. — พิมพ์ค่า union ผิด · นี่คือค่าตัวจริงของการเขียน union แทน string
TS2532Object is possibly 'undefined'. — ขึ้นเฉพาะตอนเปิด --noUncheckedIndexedAccess เพิ่ม
TS2550Property '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. — method ใหม่กว่า --target ที่ตั้งไว้

💬 ถามได้เจอ error ที่ไม่มีในตารางนี้ ส่งมาสามอย่าง — โค้ดที่รัน · คำสั่งเต็ม ๆ ที่พิมพ์ · ผลที่ได้แบบก็อปมาทั้งบรรทัด — เดี๋ยวรันซ้ำแล้วเพิ่มลงตาราง · ทุกบรรทัดในตารางนี้มาจากบท 63–68 ที่รันเจอจริงทั้งหมด

12 · เทียบกับ Go สั้น ๆ

เรื่องGoTypeScriptสิ่งที่เปลี่ยนไปจริง
คอมไพล์ไม่ผ่าน go build ไม่ผ่าน = ไม่มีไฟล์ให้รัน tsc ฟ้องแล้วยังสร้าง .js ให้อยู่ดี (exit 2) ต้องให้ CI กันเอง — “โค้ดรันได้” ไม่ได้แปลว่า “type ถูก”
type ตอนรัน อยู่กับข้อมูลตลอด (reflect · type switch) ถูกลบทิ้งหมด (type erasure) instanceof Score เขียนไม่ได้ → TS2693 · ข้อมูลจาก API ต้องเขียนโค้ดตรวจเองตอนรัน
ตัดสินว่า type เข้ากันได้ ดูชื่อ (nominal typing) ดูรูปร่าง (structural typing) สอง type คนละชื่อที่รูปร่างตรงกัน สลับกันได้เลย ไม่ต้องแปลงชนิด
ชนิดตัวเลข int8 … int64 · uint… · float64 มี number ตัวเดียว 10 / 3 ได้ 3.3333333333333335 ไม่ใช่ 3 · อยากได้ 3 ต้องเรียก Math.trunc()
field ที่ไม่ได้ใส่ค่า ได้ zero value ("" สำหรับ string) ได้ undefined — ช่องที่ไม่มีคือไม่มีจริง ๆ ต้องเช็ค undefined เอง ไม่ใช่เช็คค่าว่าง
slice s1[1:3] = หน้าต่างที่มองไปยังก้อนเดิม — เขียนทับแล้วของเดิมเปลี่ยนตาม s1.slice(1, 3) = ก๊อปออกมาเป็นก้อนใหม่ ชื่อคำสั่งเหมือนกัน แต่ผลตรงข้ามกัน — แต่ TypeScript ก๊อปชั้นเดียว object ข้างในยังแชร์
array ที่ปนชนิด ต้องใช้ []any แล้ว type assert ทีหลัง โดนฟ้องตั้งแต่บรรทัดที่ประกาศ (ถ้าระบุ type ไว้) ตรงนี้ TypeScript เข้มกว่าที่คน Go คาด — อย่าเผลอปล่อยเป็น Array<any>
0.1 + 0.2 fmt.Println(0.1 + 0.2) ได้ 0.3
แต่ผ่านตัวแปร float64 ได้ 0.30000000000000004
ได้ 0.30000000000000004 เสมอ เก็บเหมือนกันทุกบิต · ที่ต่างคือ Go มีขั้นตอนคำนวณค่าคงที่ตอนคอมไพล์ (untyped constant) — ทั้งสองภาษาห้ามเก็บเงินเป็นทศนิยม ให้เก็บเป็นสตางค์

13 · ตารางกับดักรวมทั้งกลุ่ม

อาการที่เห็นสาเหตุทางแก้
tsc ฟ้อง แต่ไฟล์ .js ยังถูกสร้าง แล้วโปรแกรมรันไปเรื่อย ๆ tsc แยกงาน “ตรวจ” กับ “แปลง” ออกจากกัน · แจ้ง exit code 2 ไว้แค่นั้น ให้ CI สั่ง tsc --noEmit แล้วดู exit code · หรือเปิด noEmitOnError
ตัวแปรประกาศ : number แต่ typeof ตอนรันได้ string มี any อยู่ต้นสาย (จาก API · JSON.parse · ฟังก์ชันที่คืน any) เปลี่ยนต้นสายเป็น unknown แล้ว narrow · type คือคำสัญญา ไม่ใช่ยามที่เฝ้าตอนรัน
"5" + 10 ได้ "510" · "3" + 1 ได้ "31" ใน JavaScript + ที่มี string อยู่ข้างหนึ่ง = ต่อข้อความ ไม่ใช่บวก แปลงเป็นเลขก่อนบวก · (เกร็ด: - ทำตรงข้าม — "3" - 1 ได้ 2)
ราคารวมบนหน้าจอขึ้นคำว่า NaN เอา undefined ไปคำนวณ — มาจาก optional ที่ไม่ได้เช็ค · as ที่โกหก · หรือ acc[key] ที่ยังไม่มีค่า เช็คด้วย if · ใส่ ?? 0 · NaN ติดเชื้อ — จุดที่เห็นไม่ใช่จุดที่มันเกิด
หน้าเว็บขาว ตอนผู้ใช้คนแรกที่ยังไม่มีข้อมูลเปิดเข้ามา reduce ที่ไม่ใส่ค่าเริ่มต้น + array ว่าง → TypeError: Reduce of empty array with no initial value ใส่ค่าเริ่มต้นให้ reduce ทุกครั้ง ไม่ต้องรอให้มีเหตุผล
ผลรวมกลายเป็นข้อความยาว ๆ อย่าง "15340" ค่าเริ่มต้นของ reduce เป็น "" แทนที่จะเป็น 0 · tsc ไม่ฟ้องเพราะถูกกฎหมาย type ของ reduce มาจากค่าเริ่มต้น — ตรวจตัวสุดท้ายหลังจุลภาคเสมอ
map คืน [undefined, undefined, undefined] callback มีปีกกาแล้วลืม return — (n) => { n * 2; } เอาปีกกาออก หรือเขียน return · เห็นคำว่า void ใน error = ไปหาปีกกาที่ลืม return
ตารางอีกหน้าจอหนึ่งสลับลำดับไปเองโดยไม่มีใครสั่ง sort() เรียงของเดิมแล้วคืนก้อนเดิม · tsc --strict เงียบสนิท ก็อปก่อนเรียงเสมอ: [...arr].sort(…) หรือ toSorted() (ต้อง target es2023)
ตัดรายการมาแค่บางส่วน แล้วรายการต้นทางหายไปเลย พิมพ์ splice แทน slice — ต่างกันตัว p ตัวเดียว และมีอยู่จริงทั้งคู่ จำว่า splice(เริ่ม, จำนวนที่ลบ) · slice(เริ่ม, ก่อนถึง) · เขียนเทสว่า arr.length ก่อน/หลังเท่ากัน
ก๊อป array ออกมาแก้ แล้วของเดิมเปลี่ยนตาม slice / [...arr] เป็น shallow copy — object ข้างในยังเป็นชิ้นเดียวกัน ก๊อป object ข้างในด้วย หรือใช้ structuredClone()
เทียบ === "Bob" แล้วไม่เท่าทั้งที่ตาเห็นว่าเหมือนกัน split(",") ทิ้งเคาะไว้ติดหน้าชื่อ ได้ ' Bob' ต่อ .map(n => n.trim()) ทีหลัง — อย่าไปเดา delimiter ให้เป๊ะ
นับได้ 4 ชิ้น ทั้งที่มีของจริง 2 ผู้ใช้พิมพ์คอมมาท้ายบรรทัด → ",a,b,".split(",") ได้ [ '', 'a', 'b', '' ] ต่อ .filter(n => n !== "") · และรู้ว่า "".split(",") ยาว 1 แต่ "".split("") ยาว 0
ไปหยิบ [1] มาใช้แล้วได้ undefined split ที่หา delimiter ไม่เจอไม่ error และไม่คืนของว่าง — คืน array ยาว 1 ที่มีทั้งก้อน เช็ค .length ก่อนหยิบ index · โปรแกรมจะไม่พังตรงนั้น แต่ไปพังอีกสิบบรรทัดถัดไป
ข้อความไทยหรือ emoji แตกเป็นชิ้นแปลก ๆ split("") แบ่งตามหน่วยในหน่วยความจำ ไม่ใช่ตามที่ตาเห็น — "กิน" ได้ 3 ชิ้น อย่าใช้ split("") นับความยาว/กลับด้าน · ตัดข้อความยาวด้วย slice ก็ระวังตัดกลางสระ
ข้อมูลจาก API มีช่องเกินมา แต่ไม่มีใครฟ้อง excess property check ใช้เฉพาะกับก้อนที่พิมพ์สดตรงนั้น ไม่ใช่ก้อนที่ผ่านตัวแปรมา อย่าคาดหวังว่า TypeScript จะกรองช่องแปลกปลอมให้ — ต้อง validate ตอนรันเอง
ไฟล์ที่เคยผ่านเมื่อวาน วันนี้พังทั้งที่ไม่ได้แก้ มีคนประกาศ interface ชื่อเดียวกันเพิ่มที่อื่น (declaration merging) ใช้ merging เฉพาะตอนต่อเติม type ของ library · โค้ดของตัวเองใช้ type ที่ชื่อซ้ำไม่ได้
สินค้ากับผู้ใช้สลับกันได้โดยไม่มี error structural typing — { id: string; name: string } ของสองอย่างหน้าตาเหมือนกันเป๊ะ ไม่ใช่ความผิดของ compiler — แปลว่า type ยังไม่ได้บรรยายความต่างที่แท้จริงออกมาเป็นรูปร่าง
พิมพ์ค่าสถานะผิดตัวเดียว แล้วยอดออกมาเป็น 0 เงียบ ๆ ประกาศเป็น status: string เฉย ๆ แทนที่จะเป็น union ของค่าที่มีจริง เขียนเป็น "delivered" | "pending" | "cancelled" แล้วพิมพ์ผิดจะได้ TS2367 ทันที
this กลายเป็น undefined ทั้งที่เขียนอยู่ในตัว object ห่างกันสองบรรทัด ส่ง function ปกติเข้าไปให้ forEach เรียก · this ตัดสินตอนถูกเรียก callback ใช้ arrow function · method ของ object ใช้ทรงปกติ
อยากได้ “ตัวแรกที่ใช่แล้วหยุด” แต่มันเดินครบทุกแถว map · filter · reduce หยุดกลางทางไม่ได้ ใช้ find · some · every หรือ for ไปเลย
เจอโจทย์ “top N” แล้วเผลอเขียน .filter(m => m.rating >= 8.5) filter ตัดสินทีละชิ้นจากตัวมันเอง แต่ “top N” ต้องรู้ทั้งกองก่อน [...list].sort(…).slice(0, n) · อย่าฮาร์ดโค้ดเกณฑ์ที่เดามาจากข้อมูลตัวอย่าง
129.9 * 3 ได้ 389.70000000000005 ทศนิยมฐานสองเก็บ 0.1 แบบเป๊ะ ๆ ไม่ได้ · บางคู่ลงตัว บางคู่ไม่ และเดาล่วงหน้าไม่ได้ ปัดตอนแสดงผล (Math.round(x * 100) / 100) หรือเก็บเป็นสตางค์ (จำนวนเต็ม) ตั้งแต่ต้น

14 · อ้างอิง