Перейти к основному содержимому

TypeScript и Node.js

Разработчику Архитектору

Дальше: TypeORM · Асинхронность · Обработка ошибок · Форматы и подключение


Node.js выполняет скомпилированный JavaScript; TypeScript задаёт контракты для HTTP API, файлов, БД и доменной логики. Один язык на frontend и backend упрощает общие типы DTO — при дисциплине слоёв.

Маршрут: Первая программаФорматы и подключениеNodeTypeORM.

Runtime Node: раздел Node в JS. Монорепо — 3.md.


TypeScript на backend

ВыгодаПример
Контракт APICreateUserDto vs UserEntity
Рефакторингпереименование поля ловит все handlers
CItsc --noEmit на каждый PR
Общие типыпакет @myorg/contracts в monorepo

TS не отменяет валидацию тела запроса: JSON из сети — unknown до проверки — 6.md.


Минимальный проект

mkdir my-api && cd my-api
npm init -y
npm i -D typescript @types/node tsx
npx tsc --init

tsconfig.json:

{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"outDir": "dist",
"rootDir": "src",
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}

package.json:

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

Разбор:

  • NodeNext — ESM/CJS по правилам Node 20+ — 9.md.
  • tsx — dev без ручной пересборки; production — tsc + node.

Слои: DTO, domain, persistence

// types/dto.ts — контракт HTTP
export type CreateUserDto = {
email: string;
name: string;
};

// domain/user.ts — бизнес-сущность
export type User = {
id: string;
email: string;
name: string;
createdAt: Date;
};

// mappers
export function fromDto(dto: CreateUserDto): Omit<User, "id" | "createdAt"> {
return { email: dto.email.trim(), name: dto.name.trim() };
}

Разбор:

  • DTO отражает тело запроса; domain — то, с чем работает сервис.
  • Поля БД (created_at) не протекают в API без маппера.

Сервис и Result

type ServiceError = "validation" | "conflict";
type ServiceResult<T> =
| { ok: true; value: T }
| { ok: false; error: ServiceError };

export function createUser(
input: CreateUserDto,
existingEmails: Set<string>,
): ServiceResult<User> {
if (!input.email.includes("@")) {
return { ok: false, error: "validation" };
}
if (existingEmails.has(input.email)) {
return { ok: false, error: "conflict" };
}
const user: User = {
id: crypto.randomUUID(),
...fromDto(input),
createdAt: new Date(),
};
return { ok: true, value: user };
}

Подробнее Result — 27.md.


Handler в стиле Express

import type { Request, Response } from "express";
import type { CreateUserDto } from "./types/dto.js";

type ApiOk<T> = { ok: true; data: T };
type ApiErr = { ok: false; error: string };
type ApiResponse<T> = ApiOk<T> | ApiErr;

function isCreateUserDto(body: unknown): body is CreateUserDto {
if (typeof body !== "object" || body === null) return false;
const o = body as Record<string, unknown>;
return typeof o.email === "string" && typeof o.name === "string";
}

export async function postUser(
req: Request,
res: Response<ApiResponse<User>>,
): Promise<void> {
if (!isCreateUserDto(req.body)) {
res.status(400).json({ ok: false, error: "Invalid body" });
return;
}
const result = createUser(req.body, new Set());
if (!result.ok) {
const status = result.error === "conflict" ? 409 : 400;
res.status(status).json({ ok: false, error: result.error });
return;
}
res.status(201).json({ ok: true, data: result.value });
}

Разбор:

  • bodyunknown до guard.
  • Ответ типизирован union — клиент на TS может переиспользовать тип.

NestJS, Fastify — те же принципы DTO; декораторы — 23.md.


Файлы: fs/promises

import { readFile, writeFile } from "node:fs/promises";
import path from "node:path";

async function loadConfig(dir: string): Promise<Record<string, string>> {
const file = path.join(dir, "config.json");
const raw = await readFile(file, "utf8");
const data: unknown = JSON.parse(raw);
if (typeof data !== "object" || data === null) {
throw new Error("Invalid config");
}
return data as Record<string, string>;
}

Разбор:

  • @types/node даёт типы для fs, path, crypto.
  • Async — 17.md.

Структура backend-проекта

src/
├── index.ts # bootstrap
├── app.ts # HTTP app
├── modules/
│ └── users/
│ ├── user.service.ts
│ ├── user.routes.ts
│ └── user.types.ts
└── shared/
├── errors.ts
└── types/
СлойОтветственность
routes / controllersHTTP, статусы
servicesбизнес-логика, Result
repositoriesБД — 26.md

Переменные окружения

function requireEnv(name: string): string {
const v = process.env[name];
if (!v) throw new Error(`Missing env: ${name}`);
return v;
}

const port = Number(requireEnv("PORT"));

Для сложных конфигов — Zod-схема env — 6.md.


CI

- run: npm ci
- run: npm run typecheck
- run: npm test
- run: npm run build

Разбор:

  • typecheck и build — разные шаги, если bundler не вызывает tsc3.md.

Частые ошибки

ОшибкаЧто делать
req.body as Dtotype guard
Один тип на DTO и ORM entityразделить + mapper
CommonJS/import без .jsNodeNext + расширение в import
Нет @types/nodedevDependency
any в middlewareтипизировать Request augmentation

Практика

  1. Поднимите API с одним POST /users и guard тела.
  2. Разделите CreateUserDto, User, ответ API.
  3. Добавьте typecheck в CI.
  4. Прочитайте файл конфига через fs/promises с unknown + проверкой.
  5. Верните 409 при conflict из ServiceResult.

Смежные статьи