- có một form để tạo bài
- một draft tự động lưu
- một row trong database
- và một kiểu dữ liệu sạch để dùng trong app.
Post.
Cách nhiều người xử lý là… định nghĩa bốn type riêng biệt, ở mỗi nơi cần, rồi mỗi cái copy lại title, body, id theo cách riêng.
Hoặc ngược lại, dồn tất cả vào một Post khổng lồ rồi dùng Partial<Post> cho mọi thứ. Cả hai đều có vấn đề: một cái thì lặp lại quá nhiều, một cái thì mơ hồ đến mức TypeScript không còn giúp được bạn nữa.
Bài này đi theo hướng pro-level hơn: xây dựng một bộ “từ vựng cơ bản” nhỏ, rồi compose ra các type cần thiết từ đó.
Bước 1: Định nghĩa các mảnh ghép một lần
Thay vì nghĩ “tôi cần typePost”, hãy hỏi: Post được cấu thành từ những nhóm thông tin nào?
PostCore— nội dung bài viết, do người dùng nhập vàoPostMeta— metadata hệ thống tạo ra, không phải người dùngPostPersistence— trạng thái lưu trữPostId,PostStatus— primitive được đặt tên, dùng lại ở nhiều nơi
Bước 2: Compose type chính từ các mảnh đó
Post giờ là tập hợp của ba nhóm, tức là:
title,body— từPostCoreid,createdAt— từPostMetastatus— từPostPersistence
excerpt vào PostCore, tất cả các type dùng PostCore đều cập nhật theo.
Bước 3: Tạo type cho form tạo Post
Form tạo Post chỉ cho người dùng nhậptitle và body. Không cần id, không cần createdAt, không cần status. Vậy thì:
PostCore thay đổi, PostFormInput thay đổi theo, đúng theo đúng nghĩa của nó.
Bước 4: Tạo type cho draft
Draft là trạng thái trung gian — bài chưa được publish nhưng đã có ID để lưu tạm. Có hai biến thể thường gặp: Draft đã có ID nhưng chưa có đủ metadata:id để lưu nháp:
Pick giúp lấy chính xác field cần thiết từ một type có sẵn mà không cần khai báo lại. Partial giúp làm cho tất cả các field trở thành optional — thích hợp cho các thao tác cập nhật từng phần.
Bước 5: Tạo type cho DB row
Database không phải lúc nào cũng có cùng shape với app. Ví dụ, cột nullable trong SQL sẽ trả vềnull, không phải undefined:
Post là “dữ liệu đã được xử lý và tin cậy” ở tầng app.
Mapping functions: liên kết các type với nhau
Các type đã được định nghĩa riêng — giờ cần các hàm để chuyển đổi giữa chúng. Đây là cách chúng liên kết, không phải bằng kế thừa, mà bằng composition + mapping functions:rowToPost xử lý null safety tại đúng một chỗ — biên giới giữa DB và app. Sau khi qua hàm này, Post sẽ không bao giờ có null trong title hay body nữa, và TypeScript biết điều đó.
Khi nào dùng Pick, Omit, Partial
Các utility types này hữu ích cho các biến thể nhỏ, nhưng không nên là thiết kế chính:
Pick<Post, ...>— tạo view model nhỏ, ví dụ cho danh sách bài viếtOmit<Post, ...>— tạo payload cho API create, loại bỏ field do server sinh raPartial<PostCore>— input cho patch/update, rõ ràng về phạm vi cho phép thay đổi
Partial<Post>cho mọi thứ — mơ hồ, TypeScript không thể cảnh báo khi thiếu field quan trọng
Cấu trúc file gọn gàng
Toàn bộ các type trên nằm trong một file duy nhất:src/types/post.ts
Nguyên tắc thực tiễn
Nên extract:- Nhóm field có ý nghĩa ổn định như
PostCore,PostMeta - Primitive dùng lại ở nhiều nơi như
PostId,PostStatus
- Mọi thứ vào “generic wizardry” trừu tượng quá mức
- Type chỉ để tiết kiệm 2 dòng, không có nghĩa rõ ràng
Tóm tắt
| Type | Cấu thành từ | Dùng ở đâu |
|---|---|---|
Post | PostCore & PostMeta & PostPersistence | App logic, render |
PostFormInput | PostCore | Form component |
PostDraft | PostCore & Pick<PostMeta, 'id'> | Auto-save, draft API |
PostDraftPatch | Partial<PostCore> | Partial update |
PostRow | Định nghĩa riêng | DB layer |
PostSummary | Pick<Post, ...> | List view |
author, tags, seo — bạn chỉ cần thêm building block mới và compose tiếp, không cần đập đi làm lại.
Bước tiếp theo có thể là kết hợp pattern này với Zod để validation schema cũng được compose từ cùng các mảnh đó, hoặc dùng drizzle-orm để type DB row được infer tự động thay vì viết tay.