การตั้งค่า TypeScript ให้โปรเจกต์ Node.js ใช้งานได้จริงต้องมีสามอย่าง คือ compiler (tsc) สำหรับแปลงไฟล์ .ts เป็น JavaScript ที่จะใช้บน production, เครื่องมือที่รันไฟล์ .ts ได้ตรง ๆ ระหว่างพัฒนา และตัวที่ restart แอปให้ทุกครั้งที่กด save ชุดที่ใช้กันมานานคือ typescript, ts-node และ nodemon ติดตั้งเป็น dev dependency ทั้งหมด แล้วผูกเข้าด้วยกันด้วย tsconfig.json กับ script ใน package.json อีกไม่กี่บรรทัด
บทความนี้ตั้งค่าทุกอย่างตั้งแต่โฟลเดอร์ว่าง อธิบาย option ใน tsconfig.json ที่มีผลจริง และแก้ข้อผิดพลาดที่เจอบ่อยใน script start ช่วงท้ายจะเทียบ ts-node กับทางเลือกที่ใหม่กว่าอย่าง tsx และการ strip type ที่มีใน Node เอง เพื่อให้เลือกได้ว่าอะไรเหมาะกับโปรเจกต์ของคุณ
ECMAScript, JavaScript และ TypeScript
สามชื่อนี้หมายถึงคนละอย่างกัน:
| ชื่อ | คืออะไร |
|---|---|
| ECMAScript | spec ของภาษา ออกใหม่ทุกปีโดย TC39 (ES2015, ES2022, ES2025...) กำหนด syntax และ built-in ต่าง ๆ |
| JavaScript | ภาษาที่ engine อย่าง V8 (Chrome, Node.js) และ SpiderMonkey (Firefox) implement ตาม ECMAScript |
| TypeScript | superset ของ JavaScript จาก Microsoft ที่เพิ่ม static type เข้ามา ไฟล์ JavaScript ที่ถูกต้องทุกไฟล์ถือเป็น syntax ของ TypeScript ที่ถูกต้องด้วย |
TypeScript ไม่ได้รันใน engine โดยตรง compiler จะตรวจ type ก่อน จากนั้น ลบ type ทิ้งแล้วได้ JavaScript ธรรมดาออกมา option target เป็นตัวกำหนดว่า output จะใช้ ECMAScript เวอร์ชันไหน type มีอยู่แค่ตอนพัฒนาเท่านั้น TypeScript จึงตรวจข้อมูลที่เข้ามาจาก API ตอน runtime ไม่ได้ ส่วนนั้นยังต้องมี runtime validation อยู่
ถ้าอยากรู้ว่าระบบ type ให้อะไรบ้าง อ่านต่อได้ที่ TypeScript basic types: any, unknown และ never
สิ่งที่ต้องมีก่อนตั้งค่า TypeScript
- Node.js เวอร์ชัน LTS ปัจจุบัน ตรวจด้วย
node -v - Package manager ตัวอย่างในบทความใช้ Yarn ถ้าใช้ npm ให้เทียบตามตารางนี้:
| งาน | Yarn | npm |
|---|---|---|
สร้าง package.json | yarn init -y | npm init -y |
| เพิ่ม dev dependency | yarn add -D typescript | npm install -D typescript |
| รัน binary ในโปรเจกต์ | yarn tsc | npx tsc |
| รัน script | yarn dev | npm run dev |
ขั้นที่ 1: สร้างโปรเจกต์
mkdir my-service && cd my-service
yarn init -y
mkdir srcyarn init -y สร้าง package.json ด้วยค่า default โดยไม่ถามคำถาม ถ้าใช้ Yarn 2 ขึ้นไป แค่ yarn init ก็ได้ผลเหมือนกัน
ขั้นที่ 2: ติดตั้ง TypeScript
yarn add -D typescript @types/nodeflag -D บันทึกเป็น devDependencies เพราะใช้แค่ตอน build และตอนพัฒนา JavaScript ที่ compile แล้วบน production ไม่ได้ import แพ็กเกจเหล่านี้ การแยกออกจาก dependencies ทำให้การติดตั้งบน production เล็กลง
@types/node คือ type definition ของ built-in ใน Node เช่น process, Buffer และ node:fs ถ้าไม่มี process.env.PORT จะ error ว่า "Cannot find name 'process'" ส่วนกลไกของแพ็กเกจ @types อธิบายไว้ใน declaration file และแพ็กเกจ @types
การติดตั้ง TypeScript แยกในแต่ละโปรเจกต์แทนการติดตั้งแบบ global ทำให้ developer ทุกคนและ CI ใช้ compiler เวอร์ชันเดียวกัน ตามที่ระบุไว้ใน package.json
ขั้นที่ 3: สร้างและแก้ tsconfig.json
yarn tsc --initคำสั่งนี้สร้าง tsconfig.json ให้ เนื้อหาจะต่างกันตามเวอร์ชันของ TypeScript รุ่น 5.x ช่วงแรกจะได้ไฟล์ยาวที่ option ส่วนใหญ่ถูก comment ไว้ ส่วนรุ่นใหม่กว่าจะได้ไฟล์สั้นกว่าและเข้มงวดกว่า ที่ออกแบบมาสำหรับ module สมัยใหม่ ไม่ว่าจะได้แบบไหน ควรแทนที่ด้วย config ที่เราเข้าใจทุกบรรทัด สำหรับ service บน Node.js:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2023"],
"types": ["node"],
"rootDir": "src",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"sourceMap": true
},
"include": ["src"]
}option ใน tsconfig ที่ควรเข้าใจ
| Option | ทำอะไร | ทำไมตั้งค่านี้ |
|---|---|---|
target | เวอร์ชัน ECMAScript ของ JavaScript ที่ได้ออกมา | Node LTS ปัจจุบันรองรับ syntax ของ ES2022 อยู่แล้ว ไม่ต้องแปลงลง |
module / moduleResolution | วิธี emit และ resolve import | NodeNext ทำตามกฎจริงของ Node ทั้ง CommonJS และ ES module |
lib | API built-in ที่ type checker รู้จัก | ให้ตรงกับ runtime และไม่มี type ของ DOM ในโปรเจกต์ฝั่ง server |
types | แพ็กเกจ @types/* ที่โหลดอัตโนมัติ | เฉพาะของ Node ถ้าติดตั้งตัวอื่นเช่น jest ค่อยเพิ่มเข้าไป |
rootDir / outDir | ตำแหน่ง source และตำแหน่ง output | แยก src/ กับ dist/ ออกจากกัน |
strict | เปิด strict check ทั้งหมด รวมถึง strictNullChecks และ noImplicitAny | ประโยชน์ส่วนใหญ่ของ TypeScript มาจากตรงนี้ ควรเปิดตั้งแต่วันแรก |
esModuleInterop | ทำให้เขียน import express from 'express' กับแพ็กเกจ CommonJS ได้ | ไม่ต้องใช้ import * as แก้ขัด |
skipLibCheck | ข้ามการตรวจ type ของไฟล์ .d.ts ใน node_modules | build เร็วขึ้น และลด error จาก type ของ library ภายนอก |
sourceMap | สร้างไฟล์ .js.map | stack trace และ debugger ชี้กลับมาที่บรรทัดในไฟล์ .ts |
เมื่อใช้ module: "NodeNext" รูปแบบ output จะขึ้นกับ package.json ถ้าไม่มี field "type" ไฟล์จะถูก compile เป็น CommonJS ซึ่งใช้กับ ts-node ง่ายที่สุด ถ้าใส่ "type": "module" จะได้ ES module และ relative import ต้องมีนามสกุล .js ด้วย (import { db } from './db.js')
ขั้นที่ 4: เขียนโค้ดแล้ว compile ด้วย tsc
// src/index.ts
import { createServer } from 'node:http';
const port = Number(process.env.PORT ?? 3000);
const server = createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true, path: req.url }));
});
server.listen(port, () => {
console.log(`Listening on http://localhost:${port}`);
});compile แล้วรัน output:
yarn tsc
node dist/index.jstsc อ่าน tsconfig.json ตรวจ type ทุกไฟล์ใน src แล้วเขียน dist/index.js พร้อม source map ถ้ามี type error มันจะรายงาน แต่โดย default ยังคง emit output ออกมา ถ้าอยากให้ build ที่ fail ไม่มี output เลย ให้เพิ่ม "noEmitOnError": true
ขั้นที่ 5: รัน TypeScript ตรง ๆ ด้วย ts-node
การ compile ก่อนรันทุกครั้งช้าเกินไปสำหรับช่วงพัฒนา ts-node compile ใน memory แล้วรันผลลัพธ์ในขั้นตอนเดียว:
yarn add -D ts-node
yarn ts-node src/index.tsโดย default ts-node จะตรวจ type ทุกไฟล์ที่โหลด ทำให้ startup ช้าลงในโปรเจกต์ใหญ่ ถ้าต้องการปิดตอนพัฒนาแล้วปล่อยให้ editor กับ CI ตรวจ type แทน ให้เพิ่ม section ts-node ไว้ระดับบนสุดของ tsconfig.json ข้าง ๆ compilerOptions:
"ts-node": {
"transpileOnly": true
}ถ้าเลือกทางนี้ ต้องเพิ่ม script typecheck (ดูด้านล่าง) แล้วรันใน CI ไม่อย่างนั้น type error อาจหลุดเข้า main branch โดยไม่มีใครเห็น
ขั้นที่ 6: restart อัตโนมัติด้วย nodemon
nodemon เฝ้าดูไฟล์และ restart process เมื่อไฟล์เปลี่ยน:
yarn add -D nodemonตั้งค่าใน nodemon.json ที่ root ของโปรเจกต์:
{
"watch": ["src"],
"ext": "ts,json",
"ignore": ["src/**/*.test.ts"],
"exec": "ts-node ./src/index.ts"
}watch: ดูเฉพาะโฟลเดอร์ source ไม่รวมnode_modulesหรือdistext: นามสกุลไฟล์ที่ทำให้ restartignore: แก้ไฟล์ test แล้วไม่ควรทำให้ server restartexec: คำสั่งที่ nodemon รัน ในที่นี้คือ ts-node พร้อมไฟล์ entry
ขั้นที่ 7: script ใน package.json
{
"name": "my-service",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "nodemon",
"build": "tsc",
"start": "node dist/index.js",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@types/node": "^22.0.0",
"nodemon": "^3.1.0",
"ts-node": "^10.9.2",
"typescript": "^5.6.0"
}
}เวอร์ชันที่คุณติดตั้งได้อาจต่างจากนี้ ไม่เป็นไร จุดสำคัญคือ start บาง setup เขียนไว้ว่า "start": "node src/index.js" ซึ่งรันไม่ได้ เพราะใน src/ มีแต่ไฟล์ .ts production ต้องรัน output ที่ compile แล้ว ดังนั้น start ต้องชี้ไปที่ dist/index.js และต้องรัน build ก่อนเสมอ
ขั้นตอนที่ใช้ทุกวัน:
yarn devระหว่างพัฒนา save ไฟล์แล้ว server จะ restart เองyarn typecheckก่อน commit และใน CIyarn build && yarn startบน production หรือใน Docker image
เพิ่ม output ของ build ลงใน .gitignore:
node_modules/
dist/ตอนนี้โปรเจกต์จะมีหน้าตาแบบนี้:
my-service/
├── src/
│ └── index.ts
├── dist/ # generated by tsc
├── nodemon.json
├── package.json
└── tsconfig.jsonทางเลือกอื่นแทน ts-node และ nodemon
ts-node ยังใช้งานได้ แต่ไม่ใช่ตัวเลือกเดียวอีกต่อไป และการใช้กับ ES module ต้องตั้งค่าเพิ่ม ปัจจุบันมีสองทางเลือกที่ใช้กันแพร่หลาย:
| เครื่องมือ | ตรวจ type ไหม | Watch mode | หมายเหตุ |
|---|---|---|---|
tsc | ตรวจ | tsc --watch (compile อย่างเดียว) | ใช้สร้าง output สำหรับ production |
ts-node + nodemon | ตรวจ ยกเว้นเปิด transpileOnly | ผ่าน nodemon | ง่ายที่สุดกับ CommonJS |
tsx | ไม่ตรวจ | tsx watch src/index.ts | ใช้ esbuild เร็ว รองรับทั้ง CommonJS และ ES module |
node แบบ type stripping | ไม่ตรวจ | node --watch src/index.ts | มีใน Node 22.18+ และ 23.6+ รองรับเฉพาะ syntax ที่ลบทิ้งได้ ใช้ enum, namespace หรือ parameter property ใน constructor ไม่ได้ |
ถ้าใช้ tsx script สำหรับ dev จะเหลือบรรทัดเดียว และไม่ต้องใช้ nodemon:
{
"scripts": {
"dev": "tsx watch src/index.ts"
}
}ตัวรันแบบเร็วทุกตัวไม่ตรวจ type ไม่ว่าเลือกตัวไหน ต้องเก็บ tsc --noEmit ไว้ใน CI เสมอ สำหรับโปรเจกต์ใหม่ tsx หรือ Node แบบ native มักง่ายกว่า ส่วนโปรเจกต์เดิมที่ใช้ ts-node อยู่และทำงานได้ดี ก็ไม่มีเหตุผลเร่งด่วนที่ต้องย้าย
คำถามที่พบบ่อย
TypeScript ควรเป็น dependency หรือ devDependency
devDependency เพราะใช้แค่ตอน compile production รัน JavaScript ใน dist/ ซึ่งไม่ได้ import TypeScript เลย
tsc กับ ts-node ต่างกันอย่างไร
tsc compile ไฟล์ .ts เป็นไฟล์ .js ลง disk ส่วน ts-node compile ใน memory แล้วรันทันทีโดยไม่เขียนไฟล์ ใช้ tsc ตอน build และใช้ ts-node (หรือ tsx) ตอนพัฒนา
ทำไมเจอ error "Cannot find name 'process'" หรือ "Cannot find name 'require'"
ยังไม่มี type definition ของ Node.js ให้ติดตั้ง @types/node และตรวจว่า "types" ใน tsconfig.json มี "node" อยู่ หรือไม่ใส่ types เลยเพื่อให้โหลดแพ็กเกจ @types ทุกตัวที่ติดตั้งไว้
ถ้าใช้ tsx ยังต้องมี nodemon ไหม
ไม่ต้อง tsx watch restart เองเมื่อไฟล์เปลี่ยน ส่วน flag --watch ของ Node ก็ทำแบบเดียวกันเมื่อรันไฟล์ .ts ด้วย type stripping
ติดตั้ง TypeScript แบบ global แทนได้ไหม
ได้ แต่การติดตั้งในโปรเจกต์จะล็อกเวอร์ชันไว้ใน package.json ทำให้ developer ทุกคนและ CI compile ด้วยเวอร์ชันเดียวกัน รันผ่าน yarn tsc หรือ npx tsc
Checklist การตั้งค่า
-
yarn init -yและโฟลเดอร์src/ -
typescriptและ@types/nodeเป็น devDependencies -
tsconfig.jsonที่มีstrict,rootDir: "src"และoutDir: "dist" -
ts-nodeกับnodemon(หรือtsx) สำหรับช่วงพัฒนา - script
dev,build,startที่ชี้ไปที่dist/และtypecheck -
dist/และnode_modules/อยู่ใน.gitignore
เมื่อพื้นฐานพร้อมแล้ว ขั้นต่อไปคือใช้ระบบ type ให้เป็น เริ่มจาก interface กับ type alias ต่างกันอย่างไร ถ้ากำลังวางระบบ Node.js หรือ TypeScript ขนาดใหญ่และต้องการทีมที่มีประสบการณ์ Vectorkub รับพัฒนา custom software และเว็บแอป
