🧰 ตรวจด้วยอะไร เวอร์ชันไหน — ทุกบรรทัดผลรันในหน้านี้มาจาก
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 มักเหมือนเดิม — ให้เชื่อผลรันบนเครื่องคุณ ไม่ใช่ตัวหนังสือในหน้านี้
สารบัญ
- คำสั่งที่ใช้จริงทุกวัน
- type พื้นฐาน + วิธีเขียน
- interface vs type
- any vs unknown
- as (type assertion)
- ใส่ type ให้ฟังก์ชัน
- method แปลงข้อมูล 6 ตัว
- reduce — สามกับดักที่ต้องรู้
- slice vs splice
- ใครแตะของเดิมบ้าง
- ตารางแปล error ของ tsc
- เทียบกับ Go สั้น ๆ
- ตารางกับดักรวมทั้งกลุ่ม
- อ้างอิง
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 ที่บอกว่า “ไม่มีชื่อนี้”
// ไฟล์ .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
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 — อะไรทำได้/ไม่ได้ ฝั่งไหน
| สิ่งที่อยากทำ | interface | type | ผลรันที่ยืนยัน |
|---|---|---|---|
| บรรยายรูปร่าง 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.
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:
| บรรทัดในไฟล์ | ตั้งใจทำอะไร | any | unknown |
|---|---|---|---|
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=0 | 6 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 เหมือนเป็นฟังก์ชัน
ซึ่งผิดชัด ๆ ด้วยตาเปล่า
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 ที่ไหลต่อไปได้อีกไกล
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.
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; } แล้วบรรทัดล่างบวกได้เลย
=== 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 ยังเหมือนเดิมเป๊ะ
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 / some | map/filter/reduce หยุดกลางทางไม่ได้ เดินจนจบเสมอ |
| เดินสอง array พร้อมกันด้วย index เดียวกัน | for | เขียนตรงไปตรงมากว่า |
callback ต้อง await ทีละชิ้นตามลำดับ | for…of | map กับ 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 — สามกับดักที่ต้องรู้
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 ตัว
ต้นทุนของการไม่ใส่คือหน้าเว็บพังในวันที่ผู้ใช้คนแรกเปิดหน้าตะกร้าที่ยังว่างเปล่า
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 เมื่อไหร่ ให้ไปดูค่าเริ่มต้นก่อนเสมอ
?? 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 ตัว
slice | splice | |
|---|---|---|
| ของเดิม | ไม่แตะ | โดนแก้ |
| คืนอะไร | ส่วนที่ตัดมา (ก้อนใหม่) | ส่วนที่ควักทิ้ง ไม่ใช่ส่วนที่เหลือ |
| เลขตัวที่สอง | ตำแหน่งที่จะหยุดก่อนถึง | จำนวนชิ้นที่จะลบ |
| เลขติดลบ | ได้ทั้งสองตัว (นับจากท้าย) | ได้เฉพาะตัวแรก · ตัวที่สองติดลบ = ลบ 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
| เลข | แปลว่า |
|---|---|
TS2304 | Cannot find name 'int'. — ไม่มีชื่อนี้ในภาษา (คนละเรื่องกับ TS2322 ที่ “รู้จักทั้งคู่แต่ยัดใส่กันไม่ได้”) |
TS2300 | Duplicate identifier 'Status'. — ชื่อซ้ำ · ฟ้องสองบรรทัดเพราะ compiler ไม่รู้ว่าอันไหนตัวจริง · เกิดกับ type แต่ไม่เกิดกับ interface |
TS2353 | Object 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 คาดว่าจะเจอปีกกา |
TS2312 | An interface can only extend an object type or intersection of object types with statically known members. — interface ต่อยอดจาก union ไม่ได้ |
TS1015 | Parameter cannot have question mark and initializer. — ใส่ทั้ง ? และ = ค่า ที่ช่องเดียวกัน เลือกอย่างเดียว |
TS1016 | A required parameter cannot follow an optional parameter. — ช่องบังคับต้องอยู่ก่อนช่อง ? |
TS1014 | A rest parameter must be last in a parameter list. — ... ต้องเป็นช่องสุดท้าย |
TS2554 | Expected 2 arguments, but got 1. · Expected 1-2 arguments, but got 0. — จำนวน argument ไม่ตรง (อันหลังคือ split() ที่เว้นว่าง) |
TS2551 | Property '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 |
TS2769 | No overload matches this call. — บน reduce แปลว่าค่าเริ่มต้นผิด type ให้ไปดูตัวสุดท้ายหลังจุลภาคก่อน |
TS7053 | Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'. — accumulator เป็น {} เปล่า ๆ ต้องใส่ reduce<Record<…>> |
TS2365 | Operator '+' cannot be applied to types 'Order' and 'number'. — reduce ที่ลืมค่าเริ่มต้น จึงหยิบสมาชิกตัวแรกมาเป็น accumulator ·Operator '>' cannot be applied to types 'void' and 'number'. — เห็นคำว่า void ทั้งที่ไม่เคยพิมพ์เอง = ไปหาปีกกาที่ลืม return |
TS2367 | This comparison appears to be unintentional because the types '"cancelled" | "delivered" | "pending"' and '"deliverd"' have no overlap. — พิมพ์ค่า union ผิด · นี่คือค่าตัวจริงของการเขียน union แทน string |
TS2532 | Object is possibly 'undefined'. — ขึ้นเฉพาะตอนเปิด --noUncheckedIndexedAccess เพิ่ม |
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. — method ใหม่กว่า --target ที่ตั้งไว้ |
💬 ถามได้เจอ error ที่ไม่มีในตารางนี้ ส่งมาสามอย่าง — โค้ดที่รัน · คำสั่งเต็ม ๆ ที่พิมพ์ · ผลที่ได้แบบก็อปมาทั้งบรรทัด — เดี๋ยวรันซ้ำแล้วเพิ่มลงตาราง · ทุกบรรทัดในตารางนี้มาจากบท 63–68 ที่รันเจอจริงทั้งหมด
12 · เทียบกับ Go สั้น ๆ
| เรื่อง | Go | TypeScript | สิ่งที่เปลี่ยนไปจริง |
|---|---|---|---|
| คอมไพล์ไม่ผ่าน | 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 · อ้างอิง
- TypeScript Handbook — The Basics — อ่านหัวข้อ Static type-checking กับ Erased Types · เป็นที่ที่เขียนตรง ๆ ว่า type ถูกลบทิ้งก่อนรัน
- TypeScript Handbook — Everyday Types —
อ่านตั้งแต่ The primitives ถึง Optional Properties · ครอบคลุมหัวข้อ 2 ทั้งหมด และยืนยันว่าไม่มี
int/float - TypeScript Handbook — Type Compatibility — อ่านย่อหน้าแรกอย่างเดียวพอ · เป็นที่มาของ structural typing ในหัวข้อ 12
- TypeScript Handbook — Differences Between Type Aliases and Interfaces — อ่านตารางเทียบสองคอลัมน์ แล้วอ่านย่อหน้าใต้ตารางต่อ · คู่กับหัวข้อ 3
- TypeScript Handbook — Type Assertions —
อ่านสองย่อหน้าสุดท้าย ที่อธิบายว่าทำไม
as unknown asถึงลอดผ่านได้ · ตรงกับ TS2352 ในหัวข้อ 5 - TypeScript Handbook — Narrowing —
อ่านหัวข้อย่อย typeof type guards และ The in operator narrowing · คือเครื่องมือที่ใช้ปลด
unknownในหัวข้อ 4 - TypeScript Handbook — More on Functions — อ่าน Optional Parameters กับ Rest Parameters · ที่เหลือ (overload, generic) ยังไม่ต้องอ่านตอนนี้
- tsconfig — strict ·
noUncheckedIndexedAccess —
ตัวที่เกี่ยวกับหน้านี้โดยตรงคือ
strictNullChecks(TS18048) ·noImplicitAny(TS7006) ·noImplicitThis(TS2683) - MDN — Array.prototype.slice() ·
splice() —
อ่าน Parameters ของ
slice(เรื่องเลขติดลบ + shallow copy) และ Return value ของspliceสองบรรทัด - MDN — String.prototype.split() ·
Array.prototype.join() —
อ่าน Description เรื่อง empty string และเรื่อง
null/undefinedกลายเป็นข้อความว่าง - MDN — Array.prototype.reduce() —
อ่านหัวข้อ Exceptions ที่เขียนตรง ๆ ว่า
TypeErrorจะถูกโยนเมื่อ array ว่างและไม่มีค่าเริ่มต้น - MDN — Array.prototype.sort() · toSorted() — อ่านประโยคแรกของ Description ที่บอกว่ามันเรียง in place · เป็นที่มาของกับดักใหญ่ที่สุดในหัวข้อ 10
- MDN — Number ·
NaN —
อ่านย่อหน้า double-precision 64-bit binary format และหัวข้อ Testing against NaN
(
NaN === NaNเป็นfalseต้องใช้Number.isNaN()) - Go Blog — Go Slices: usage and internals — อ่านหัวข้อ Slice internals ย่อหน้าเดียว เพื่อยืนยันว่า slice ของ Go ชี้ไป array ก้อนเดิม — คนละเรื่องกับ TypeScript
- TypeScript Playground —
วางโค้ดฝั่งซ้าย แล้วกดแท็บ
.JSฝั่งขวา จะเห็น type หายไปแบบเรียลไทม์โดยไม่ต้องลงtsc