← Курс/TypeScript с Node.js#189 из 257+25 XP

TypeScript с Node.js

Зачем TypeScript в Node.js

Node.js выполняет только JavaScript. Для запуска TypeScript-кода нужно либо скомпилировать его заранее (tsc), либо использовать runtime-транспайлер.

ts-node: классический способ

ts-node — интерпретатор, который компилирует TypeScript на лету:

npm install -D ts-node typescript @types/node

# Запуск скрипта:
npx ts-node src/index.ts

# REPL:
npx ts-node

Конфигурация в tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "CommonJS",
    "moduleResolution": "node",
    "strict": true,
    "esModuleInterop": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "ts-node": {
    "transpileOnly": true
  }
}

tsx: быстрый современный способ

tsx (TypeScript Execute) — значительно быстрее ts-node, использует esbuild:

npm install -D tsx

# Запуск:
npx tsx src/index.ts

# Watch режим:
npx tsx watch src/index.ts

tsx не выполняет проверку типов — только транспиляция. Для проверки используйте tsc --noEmit отдельно.

@types/node

Без этого пакета TypeScript не знает о Node.js API:

npm install -D @types/node
import fs from 'fs'
import path from 'path'
import { createServer } from 'http'

// Теперь все Node.js типы доступны:
const server = createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' })
  res.end('Hello TypeScript!')
})

CommonJS vs ESM в TypeScript

CommonJS (классический Node.js):

{ "module": "CommonJS" }
// Компилируется в require/module.exports
import express from 'express'  // -> const express = require('express')

ESM (современный):

{ "module": "Node16" }
// Остаётся как import/export
// Файлы должны быть .mts или package.json: "type": "module"
import express from 'express'

tsconfig для Node.js проекта

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022"],
    "module": "Node16",
    "moduleResolution": "Node16",
    "rootDir": "./src",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Scripts в package.json

{
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js",
    "typecheck": "tsc --noEmit"
  }
}

Запуск TS скриптов напрямую (Node 22.6+)

Node.js 22.6+ поддерживает TypeScript без транспайлера (экспериментально):

node --experimental-strip-types script.ts

Сравнение инструментов

| Инструмент | Скорость | Typecheck | Подходит для |

|------------|----------|-----------|--------------|

| ts-node | Медленно | Да | Разработка |

| tsx | Быстро | Нет | Разработка, scripts |

| tsc + node | — | Да | Production |

| esbuild | Очень быстро | Нет | Сборка |

Примеры

Симуляция Node.js TypeScript окружения: типизированный HTTP-сервер, модули, работа с файлами — паттерны из реального TS/Node кода

// В реальном проекте это TypeScript + Node.js.
// Здесь симулируем те же паттерны в чистом JS.

// --- Симуляция типизированных Node.js модулей ---

// В TS: import { readFileSync } from 'fs'
// Тип: (path: string, encoding: 'utf-8') => string
const fs = {
  readFileSync: (filePath, encoding) => {
    // Симуляция чтения файла
    const files = {
      '/app/config.json': JSON.stringify({ port: 3000, debug: true }),
      '/app/data.txt': 'Hello, TypeScript!',
    }
    if (!files[filePath]) throw new Error(`ENOENT: no such file ${filePath}`)
    return files[filePath]
  },
  existsSync: (filePath) => {
    return ['/app/config.json', '/app/data.txt'].includes(filePath)
  }
}

// В TS: import path from 'path'
const path = {
  join: (...parts) => parts.join('/').replace(/\/+/g, '/'),
  resolve: (base, ...parts) => path.join(base, ...parts),
  extname: (filePath) => {
    const dot = filePath.lastIndexOf('.')
    return dot >= 0 ? filePath.slice(dot) : ''
  },
  basename: (filePath) => filePath.split('/').pop(),
}

// --- HTTP сервер (как express/http в TypeScript) ---

// В TS:
// interface Request { method: string; url: string; body?: unknown }
// interface Response { status: (code: number) => Response; json: (data: unknown) => void }

class MockServer {
  constructor() {
    this.routes = new Map()
    this.middleware = []
  }

  use(middleware) {
    this.middleware.push(middleware)
  }

  get(route, handler) {
    this.routes.set('GET:' + route, handler)
  }

  post(route, handler) {
    this.routes.set('POST:' + route, handler)
  }

  // Симуляция обработки запроса
  handle(method, url, body = null) {
    const req = { method, url, body, params: {}, query: {} }
    const res = {
      statusCode: 200,
      data: null,
      status(code) { this.statusCode = code; return this },
      json(data) { this.data = data; return this },
      send(text) { this.data = text; return this },
    }

    // Применяем middleware
    let idx = 0
    const next = () => {
      if (idx < this.middleware.length) {
        this.middleware[idx++](req, res, next)
      } else {
        // Ищем обработчик
        const handler = this.routes.get(method + ':' + url)
        if (handler) handler(req, res)
        else res.status(404).json({ error: 'Not Found' })
      }
    }
    next()

    return { status: res.statusCode, body: res.data }
  }
}

// --- Типизированные хендлеры (как в TS + Express) ---

// TS: interface User { id: number; name: string; email: string }
// TS: const users: User[] = [...]
const users = [
  { id: 1, name: 'Алексей', email: 'alex@example.com' },
  { id: 2, name: 'Мария', email: 'maria@example.com' },
]

const app = new MockServer()

// Logger middleware
// TS: (req: Request, res: Response, next: NextFunction) => void
app.use((req, res, next) => {
  console.log(`[LOG] ${req.method} ${req.url}`)
  next()
})

// В TS: app.get('/users', (req: Request, res: Response) => { ... })
app.get('/users', (req, res) => {
  res.json({ users, total: users.length })
})

app.get('/config', (req, res) => {
  const configPath = '/app/config.json'
  if (fs.existsSync(configPath)) {
    const data = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
    res.json(data)
  } else {
    res.status(404).json({ error: 'Config not found' })
  }
})

app.post('/users', (req, res) => {
  const newUser = { id: users.length + 1, ...req.body }
  users.push(newUser)
  res.status(201).json(newUser)
})

// --- Демонстрация ---

console.log('=== HTTP сервер ===')
let result = app.handle('GET', '/users')
console.log('GET /users:', JSON.stringify(result.body))

result = app.handle('POST', '/users', { name: 'Иван', email: 'ivan@example.com' })
console.log('POST /users:', JSON.stringify(result.body))

result = app.handle('GET', '/unknown')
console.log('GET /unknown status:', result.status)

console.log('\n=== Работа с файлами ===')
console.log('config exists:', fs.existsSync('/app/config.json'))
result = app.handle('GET', '/config')
console.log('GET /config:', result.body)

console.log('\n=== path utils ===')
console.log('join:', path.join('/src', 'utils', 'date.ts'))
console.log('extname:', path.extname('index.ts'))
console.log('basename:', path.basename('/src/utils/date.ts'))