DTOs — Data Transfer Objects in TypeScript
DTOs (Data Transfer Objects) are typed shapes for data crossing an API boundary — from client to server (request body), server to client (response), or between services. They differ from domain types: DTOs represent the wire format, while domain types represent business entities. TypeScript's utility types (`Pick`, `Omit`, `Partial`) make DTO derivation concise and maintenance-free.
DTO Patterns — Deriving from Domain Types
// ── Domain type — full internal representation ────────────────
interface Task {
readonly id: number;
title: string;
description: string | null;
status: TaskStatus;
priority: Priority;
assigneeId: number | null;
readonly createdAt: Date;
updatedAt: Date;
tags: readonly string[];
internalNotes: string; // internal only — NEVER sent to client
}
// ── Request DTOs — what the API accepts ───────────────────────
// Create: user provides required fields; server sets id/timestamps
type CreateTaskDTO = Pick<Task, "title" | "priority"> & {
description?: string;
tags?: string[];
};
// Update: all fields optional except we disallow changing timestamps
type UpdateTaskDTO = Partial<Pick<Task, "title" | "description" | "status" | "priority" | "tags">>;
// ── Response DTOs — what the API returns ──────────────────────
// Omit internal fields + present dates as ISO strings (JSON serializable)
type TaskResponseDTO = Omit<Task, "internalNotes" | "createdAt" | "updatedAt"> & {
createdAt: string; // ISO string — Date is not JSON-serializable
updatedAt: string;
};
// List response:
interface TaskListResponseDTO {
items: TaskResponseDTO[];
total: number;
page: number;
perPage: number;
}
// ── Mapper — domain → DTO ─────────────────────────────────────
function toTaskResponse(task: Task): TaskResponseDTO {
const { internalNotes, ...rest } = task; // strip internal field
void internalNotes; // explicit discard — lint would warn on unused destructure
return {
...rest,
createdAt: task.createdAt.toISOString(),
updatedAt: task.updatedAt.toISOString(),
tags: [...task.tags], // convert readonly to mutable for JSON
};
}
// ── Why DTOs separate from domain? ───────────────────────────
// 1. API shape can change without touching business logic
// 2. Strip sensitive fields (internalNotes) at the type level
// 3. Convert non-serializable types (Date → string) explicitly
// 4. Domain model can have computed properties not in the APICommon Mistakes
- Sending domain objects directly to the client — domain objects may contain sensitive fields, non-serializable types (Date), or computed properties that shouldn't be public. Always map to an explicit DTO.
- Not typing dates in DTOs as strings — `Date` objects serialize to ISO strings in JSON.stringify. If your DTO has `date: Date`, the actual JSON value is a string but TypeScript still thinks it's a Date on the receiving end. Always use `string` in response DTOs.
- Making all DTO fields optional — `Partial<Task>` for a create DTO allows sending `{}`. Use `Pick` to choose exactly the required fields, then mark only truly optional ones with `?`.
Tip
Tip
Practice DTOs Data Transfer Objects in TypeScript in small, isolated examples before integrating into larger projects. Breaking concepts into small experiments builds genuine understanding faster than reading alone.
interface for objects. type for unions and computed.
Practice Task
Note
Practice Task — (1) Write a working example of DTOs Data Transfer Objects in TypeScript from scratch without looking at notes. (2) Modify it to handle an edge case (empty input, null value, or error state). (3) Share your solution in the Priygop community for feedback.
Quick Quiz
Common Mistake
Warning
A common mistake with DTOs Data Transfer Objects in TypeScript is skipping edge case testing — empty inputs, null values, and unexpected data types. Always validate boundary conditions to write robust, production-ready typescript code.
Key Takeaways
- DTOs (Data Transfer Objects) are typed shapes for data crossing an API boundary — from client to server (request body), server to client (response), or between services.
- Sending domain objects directly to the client — domain objects may contain sensitive fields, non-serializable types (Date), or computed properties that shouldn't be public. Always map to an explicit DTO.
- Not typing dates in DTOs as strings — `Date` objects serialize to ISO strings in JSON.stringify. If your DTO has `date: Date`, the actual JSON value is a string but TypeScript still thinks it's a Date on the receiving end. Always use `string` in response DTOs.
- Making all DTO fields optional — `Partial<Task>` for a create DTO allows sending `{}`. Use `Pick` to choose exactly the required fields, then mark only truly optional ones with `?`.