PostCore, PostMeta, PostPersistence rồi compose lại. Pattern đó hoạt động tốt — nhưng có một điểm yếu: PostRow đang được viết tay.
title từ varchar(255) sang text, hoặc thêm cột mới — bạn phải nhớ cập nhật PostRow bằng tay. Nếu quên, TypeScript vẫn biên dịch được nhưng type đã lệch với thực tế.
Nếu bạn đang dùng Drizzle ORM, bạn không cần làm vậy nữa. Drizzle có thể infer PostRow trực tiếp từ schema, và mọi thay đổi ở schema sẽ tự động phản ánh vào type.
Drizzle infer type là gì?
Drizzle là một ORM cho TypeScript với triết lý “schema as code”. Bạn định nghĩa cấu trúc bảng bằng TypeScript, và Drizzle dùng chính định nghĩa đó để:- generate SQL migration
- type-check các câu query
- infer ra type của row khi đọc và khi insert
Bước 1: Schema là nguồn sự thật duy nhất
Mọi thứ bắt đầu từ đây:src/db/schema.ts
titlevàbodykhông có.notNull()— tức là DB cho phép null, và Drizzle sẽ infer chúng làstring | nullstatuscó.notNull().default('draft')— Drizzle biết cột này không bao giờ null khi đọc ra, nhưng khi insert thì optional vì đã có defaultiddùngchar(36)— phù hợp cho UUID
Bước 2: Infer DB type từ schema
src/types/post.ts
$inferSelect / $inferInsert thường được ưa dùng hơn vì type gắn trực tiếp với table, dễ đọc và không cần import thêm gì.
$inferSelect và $inferInsert khác nhau chỗ nào?
Đây là phần quan trọng. Khi Drizzle infer, nó hiểu ngữ nghĩa của từng cột dựa trên khai báo schema.
Với schema ở trên, kết quả infer ra sẽ là:
default hoặc nullable trở thành optional khi insert. Nếu viết tay, bạn rất dễ quên điều này và tạo ra type không đúng với behavior thực của DB.
Bước 3: Build các mảnh ghép từ row inferred
Thay vì tự khai báoPostId = string hay PostStatus = 'draft' | 'published', hãy derive trực tiếp từ PostRow:
id từ char(36) sang int (ví dụ), PostId tự cập nhật theo mà không cần chỉnh tay.
Bước 4: Tạo các app-level type từ những mảnh đó
DB type và app type không nên là một. DB trả về nullable, còn app-level object thường cần stricter guarantee. Đây là cách tách biệt chúng:PostRow, nhưng override title và body thành string thay vì string | null.” App đảm bảo rằng sau khi qua tầng mapping, hai trường này luôn có giá trị — TypeScript sẽ enforce điều đó.
Các type còn lại:
Bước 5: Mapping functions — mắt xích nối các type
Type chỉ là khai báo. Phần thực sự làm chúng liên kết là các hàm chuyển đổi:rowToPost là nơi xử lý null safety một lần duy nhất. Sau hàm này, tầng app không cần lo title có thể là null nữa — vì Post đã đảm bảo điều đó.
Toàn bộ chain:
File hoàn chỉnh
src/types/post.ts
PostRow giờ không bao giờ bị lệch với schema thực tế.
Nguyên tắc thực tiễn
Nên để Drizzle infer:- Row type khi đọc (
$inferSelect) - Insert type (
$inferInsert) - Primitive derive từ row như
PostId,PostStatus
- Form input type (
PostFormInput) — vì đây là UI concern, không phải DB concern - Domain type với guarantee chặt hơn DB (
Post) — vì app biết nhiều hơn DB - View model nhỏ cho UI (
PostSummary) — vì đây là presentation concern
- Viết tay
PostRowkhi đã có Drizzle — dễ lệch, khó maintain - Dùng
PostRowtrực tiếp ở tầng UI — nullable field sẽ lan khắp component - Dùng
Partial<PostRow>cho mọi thứ — mất đi tính chính xác mà TypeScript có thể đảm bảo
Tóm tắt
| Type | Dùng ở đâu | Nguồn gốc |
|---|---|---|
PostRow | $inferSelect | DB layer, mapper |
NewPostRow | $inferInsert | Drizzle insert |
PostId, PostStatus | Derive từ PostRow | Dùng lại khắp app |
PostContent | Pick<PostRow, ...> | Building block |
Post | Omit<PostRow, ...> & {...} | App logic, render |
PostFormInput | Viết tay | Form component |
PostDraft | Compose từ PostRow | Auto-save, draft API |
schema.ts, chạy migration, và TypeScript sẽ báo ngay chỗ nào trong app bị ảnh hưởng — không cần đi tìm thủ công.