Menu

Partial và Required trong TypeScript: ví dụ và DeepPartial

Partial<T> biến mọi thuộc tính của T thành tùy chọn, đúng kiểu của một object cập nhật hay patch. Tìm hiểu Partial trong hàm cập nhật, vì sao nó chỉ ở mức nông, cách viết DeepPartial, cái bẫy undefined tường minh, và kiểu ngược lại là Required<T>.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Partial<T> là một utility type có sẵn, biến mọi thuộc tính của T thành tùy chọn. Đây là kiểu tự nhiên cho một bản cập nhật hay patch: nơi gọi chỉ gửi những trường đã thay đổi.

Partial<User> là { id?: number; name?: string; email?: string }. Trình biên dịch vẫn kiểm tra những trường bạn truyền vào: { nmae: "x" } hay { name: 42 } đều là lỗi, và đó là điều làm Partial tốt hơn một tham số object lỏng lẻo hay any.

Partial được định nghĩa thế nào

Partial là một mapped type chỉ một dòng trong thư viện chuẩn của TypeScript:

type Partial<T> = {
  [P in keyof T]?: T[P];
};

Với mỗi khóa P của T, nó khai báo một thuộc tính tùy chọn có cùng kiểu. Vì ánh xạ qua keyof T, nó giữ nguyên readonly trên những thuộc tính đã có. Là một kiểu thông thường, nó không ảnh hưởng gì lúc chạy: object được truyền vào làm changes vẫn là object đó.

Đọc từ Partial nhận T | undefined

Mọi thuộc tính của Partial<T> đều có thể vắng mặt, nên đọc một thuộc tính sẽ nhận kiểu của nó cộng thêm undefined. Trình biên dịch buộc bạn xử lý trường hợp thiếu:

{ ...defaults, ...opts } là cách thường dùng để biến Partial<Options> trở lại thành Options đầy đủ: spread giá trị mặc định trước rồi để các giá trị được truyền vào ghi đè lên.

Cái bẫy undefined tường minh

Một thuộc tính tùy chọn có thể vắng mặt, nhưng cũng có thể có mặt với giá trị undefined. Object spread chép undefined đó đè lên giá trị thật, và kiểu kết quả không cho thấy điều này:

Lỗi này hay xảy ra khi bản patch được tạo từ một form hoặc query string, nơi các trường trống trở thành undefined. Tùy chọn trình biên dịch exactOptionalPropertyTypes (không thuộc strict) biến { name: undefined } thành lỗi biên dịch với name?: string, trừ khi bạn viết name?: string | undefined, nhờ đó chặn được vấn đề ngay từ gốc.

Partial chỉ ở mức nông

Partial chỉ biến các thuộc tính ở cấp ngoài cùng thành tùy chọn. Một object lồng bên trong, nếu bạn truyền vào, vẫn phải đầy đủ:

index.ts(8,3): error TS2741: Property 'tabSize' is missing in type '{ fontSize: number; }' but required in type '{ fontSize: number; tabSize: number; }'.

Thuộc tính editor là tùy chọn, nhưng khi có mặt thì nó có kiểu { fontSize: number; tabSize: number }, không đổi. Với object cài đặt, bản patch cho API và dữ liệu mẫu cho test, bạn thường muốn thuộc tính tùy chọn ở mọi cấp. Điều đó cần một kiểu đệ quy.

DeepPartial: Partial đệ quy

TypeScript không có sẵn phiên bản sâu, nhưng viết nó chỉ mất vài dòng. Hàm và mảng được giữ nguyên, vì biến phần tử mảng thành tùy chọn sẽ cho phép [undefined]:

Kiểu thì đệ quy; còn phép gộp thì không. applySettings gộp editor bằng tay vì object spread cũng chỉ ở mức nông. Hàm deep merge generic có trong các thư viện như lodash (merge), và việc khai báo kiểu cho nó khó hơn kiểu ở trên.

Required: ngược lại với Partial

Required<T> bỏ ? khỏi mọi thuộc tính. Nó được định nghĩa bằng modifier -?, và modifier này cũng loại undefined khỏi kiểu của từng thuộc tính:

Mẫu này giống với Partial, chỉ đảo ngược: người dùng API truyền vào một cấu hình lỏng, còn code bên trong làm việc với phiên bản Required mà mọi giá trị đều chắc chắn tồn tại. Phép spread ở đây có cùng lỗ hổng với hàm cập nhật ở trên: nơi gọi truyền port: undefined một cách tường minh sẽ ghi đè giá trị mặc định bằng undefined, và trình biên dịch chấp nhận điều đó trừ khi bật exactOptionalPropertyTypes. Required cũng chỉ ở mức nông giống như Partial.

Chỉ biến một số thuộc tính thành tùy chọn hoặc bắt buộc

Partial và Required áp dụng cho mọi thuộc tính. Để chỉ thay đổi vài thuộc tính, hãy tách kiểu bằng Pick và Omit rồi ghép lại:

type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;

interface Post {
  id: number;
  title: string;
  body?: string;
}

type NewPost = PartialBy<Post, "id">;      // id optional, title required, body optional
type Published = RequiredBy<Post, "body">; // body now required
KiểuTác dụngÁp dụng sâu?
Partial<T>mọi thuộc tính thành tùy chọnkhông
Required<T>mọi thuộc tính thành bắt buộc, loại bỏ undefinedkhông
DeepPartial<T> (tự viết)tùy chọn ở mọi cấpcó
PartialBy<T, K> (tự viết)chỉ các khóa K thành tùy chọnkhông
Readonly<T>mọi thuộc tính thành readonlykhông

Câu hỏi thường gặp

Partial trong TypeScript dùng để làm gì?

Partial<T> tạo một kiểu có mọi thuộc tính của T được đánh dấu tùy chọn. Với interface User { name: string; email: string }, Partial<User> là { name?: string; email?: string }, nên {}, { name: "Ada" } và một user đầy đủ đều là giá trị hợp lệ.

Partial trong TypeScript có áp dụng sâu không?

Không, Partial chỉ tác động lên các thuộc tính ở cấp ngoài cùng. Một object lồng bên trong Partial<T> vẫn phải đầy đủ. Muốn có phiên bản đệ quy, hãy viết kiểu DeepPartial<T> tự áp dụng lại cho các thuộc tính có kiểu object.

Kiểu ngược lại với Partial trong TypeScript là gì?

Required<T>. Nó bỏ ? khỏi mọi thuộc tính và cũng loại undefined khỏi kiểu của chúng, nên Required<{ port?: number }> là { port: number }. Nó được định nghĩa là một mapped type với modifier -?.

Làm sao chỉ biến một số thuộc tính thành tùy chọn?

Kết hợp Omit, Pick và Partial: type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>. PartialBy<User, "email"> giữ nguyên mọi thuộc tính, trừ email trở thành tùy chọn.

Vì sao một thuộc tính thành undefined sau khi gộp bản cập nhật Partial?

Partial<T> cho phép một thuộc tính có mặt với giá trị undefined, và object spread sao chép luôn giá trị đó: { ...user, ...{ name: undefined } } có name: undefined dù TypeScript gán cho kết quả kiểu User. Hãy lọc bỏ các giá trị undefined trước khi gộp, hoặc bật exactOptionalPropertyTypes để undefined tường minh bị từ chối.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu