กลับไปยังบทความ

Frontend

ตั้งค่า TypeScript สำหรับ Node.js ด้วย ts-node และ nodemon

ตั้งค่า TypeScript ให้โปรเจกต์ Node.js ทีละขั้น ตั้งแต่ติดตั้ง compiler ตั้งค่า tsconfig.json รันไฟล์ .ts ด้วย ts-node ให้ nodemon restart อัตโนมัติ และเขียน script ใน package.json ให้ถูกต้อง

24 กันยายน 25699 นาทีในการอ่าน

typescript project setup ts node nodemon

การตั้งค่า 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

สามชื่อนี้หมายถึงคนละอย่างกัน:

ชื่อคืออะไร
ECMAScriptspec ของภาษา ออกใหม่ทุกปีโดย TC39 (ES2015, ES2022, ES2025...) กำหนด syntax และ built-in ต่าง ๆ
JavaScriptภาษาที่ engine อย่าง V8 (Chrome, Node.js) และ SpiderMonkey (Firefox) implement ตาม ECMAScript
TypeScriptsuperset ของ 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 ให้เทียบตามตารางนี้:
งานYarnnpm
สร้าง package.jsonyarn init -ynpm init -y
เพิ่ม dev dependencyyarn add -D typescriptnpm install -D typescript
รัน binary ในโปรเจกต์yarn tscnpx tsc
รัน scriptyarn devnpm run dev

ขั้นที่ 1: สร้างโปรเจกต์

bash
mkdir my-service && cd my-service
yarn init -y
mkdir src

yarn init -y สร้าง package.json ด้วยค่า default โดยไม่ถามคำถาม ถ้าใช้ Yarn 2 ขึ้นไป แค่ yarn init ก็ได้ผลเหมือนกัน

ขั้นที่ 2: ติดตั้ง TypeScript

bash
yarn add -D typescript @types/node

flag -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

bash
yarn tsc --init

คำสั่งนี้สร้าง tsconfig.json ให้ เนื้อหาจะต่างกันตามเวอร์ชันของ TypeScript รุ่น 5.x ช่วงแรกจะได้ไฟล์ยาวที่ option ส่วนใหญ่ถูก comment ไว้ ส่วนรุ่นใหม่กว่าจะได้ไฟล์สั้นกว่าและเข้มงวดกว่า ที่ออกแบบมาสำหรับ module สมัยใหม่ ไม่ว่าจะได้แบบไหน ควรแทนที่ด้วย config ที่เราเข้าใจทุกบรรทัด สำหรับ service บน Node.js:

json
{
  "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 importNodeNext ทำตามกฎจริงของ Node ทั้ง CommonJS และ ES module
libAPI 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_modulesbuild เร็วขึ้น และลด error จาก type ของ library ภายนอก
sourceMapสร้างไฟล์ .js.mapstack 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

ts
// 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:

bash
yarn tsc
node dist/index.js

tsc อ่าน 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 แล้วรันผลลัพธ์ในขั้นตอนเดียว:

bash
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:

json
"ts-node": {
  "transpileOnly": true
}

ถ้าเลือกทางนี้ ต้องเพิ่ม script typecheck (ดูด้านล่าง) แล้วรันใน CI ไม่อย่างนั้น type error อาจหลุดเข้า main branch โดยไม่มีใครเห็น

ขั้นที่ 6: restart อัตโนมัติด้วย nodemon

nodemon เฝ้าดูไฟล์และ restart process เมื่อไฟล์เปลี่ยน:

bash
yarn add -D nodemon

ตั้งค่าใน nodemon.json ที่ root ของโปรเจกต์:

json
{
  "watch": ["src"],
  "ext": "ts,json",
  "ignore": ["src/**/*.test.ts"],
  "exec": "ts-node ./src/index.ts"
}
  • watch: ดูเฉพาะโฟลเดอร์ source ไม่รวม node_modules หรือ dist
  • ext: นามสกุลไฟล์ที่ทำให้ restart
  • ignore: แก้ไฟล์ test แล้วไม่ควรทำให้ server restart
  • exec: คำสั่งที่ nodemon รัน ในที่นี้คือ ts-node พร้อมไฟล์ entry

ขั้นที่ 7: script ใน package.json

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 ก่อนเสมอ

ขั้นตอนที่ใช้ทุกวัน:

  1. yarn dev ระหว่างพัฒนา save ไฟล์แล้ว server จะ restart เอง
  2. yarn typecheck ก่อน commit และใน CI
  3. yarn build && yarn start บน production หรือใน Docker image

เพิ่ม output ของ build ลงใน .gitignore:

text
node_modules/
dist/

ตอนนี้โปรเจกต์จะมีหน้าตาแบบนี้:

text
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:

json
{
  "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 และเว็บแอป

พร้อมสร้างสิ่งที่ยิ่งใหญ่หรือยัง?

เล่าโปรเจคของคุณให้เราฟัง — เราจะพาคุณไปถึง

เลือกคุกกี้ที่เราใช้ได้ คุกกี้ที่จำเป็นช่วยให้เว็บไซต์ทำงานได้และเปิดใช้งานเสมอ