When working with DTOs in TypeScript, I often see (and use) Pick<T>.
It’s simple, expressive, and removes a lot of boilerplate — but it also has trade-offs that are easy to overlook.
Here’s how I think about it.
What Pick<T> Is Good At
Pick<T> creates a new type by selecting fields from an existing one.
type User = {
id: string;
email: string;
passwordHash: string;
createdAt: Date;
};
type UserDTO = Pick<User, "id" | "email">;
✅ Less Duplication
I don’t have to redefine the same fields over and over.
The DTO stays automatically in sync with the domain model.
✅ Refactor-Friendly
If I rename email or change its type, TypeScript tells me immediately what breaks.
That’s a big win in real-world codebases.
✅ Great for Simple Read DTOs
For internal tools or basic CRUD APIs where the DTO closely mirrors the entity, Pick<T> works really well.
Where Pick<T> Starts to Hurt
❌ Tight Coupling to the Domain
DTOs are contracts. Pick<T> ties them directly to internal models.
type PublicUserDTO = Pick<User, "id" | "email">;
Now a change in User can accidentally become a breaking API change — even if that wasn’t the intent.
❌ Semantic Mismatch
Sometimes the API shape isn’t the same as the domain shape.
- Domain:
firstName,lastName - API:
fullName
Using Pick<T> hides the fact that transformation is required, which makes intent less clear.
How I Use It in Practice
I treat Pick<T> as a convenience, not a rule.
I’m happy to use it for:
- Internal APIs
- Read-only DTOs
- Admin panels
For public or long-lived APIs, I prefer explicit DTOs:
type PublicUserDTO = {
id: string;
email: string;
};
Sometimes I mix both approaches:
type UserBaseDTO = Pick<User, "id" | "email">;
type UserWithStatusDTO = UserBaseDTO & {
status: "active" | "inactive";
};
Final Thoughts
Pick<T> is great when the domain and the DTO mean the same thing.
It’s risky when they don’t.
I use it where it reduces noise, and avoid it where clarity and boundaries matter more.
Type safety is great — but clear intent is better.
Top comments (0)