readonly đánh dấu một thuộc tính chỉ được đặt một lần, khi tạo object, và không bao giờ bị gán lại. Readonly<T> áp dụng nó cho mọi thuộc tính của một kiểu, và readonly T[] làm điều tương tự cho mảng:
Dòng cuối cho thấy điều quan trọng nhất về readonly: nó được trình biên dịch kiểm tra, chứ không được thực thi lúc chạy. Phép gán là lỗi biên dịch (ở đây bị chặn thông báo bằng @ts-expect-error), nhưng JavaScript được sinh ra vẫn chạy nó. Nếu không có lệnh chặn, tệp sẽ không biên dịch được, và đó chính là lúc readonly làm đúng việc của nó.
Thuộc tính readonly
Đặt readonly trước tên thuộc tính trong một interface, một type literal hoặc một lớp. Thuộc tính có thể được khởi tạo nhưng không thể gán lại:
Trong một lớp, trường readonly có thể được gán ngay tại khai báo hoặc trong hàm khởi tạo, và không ở đâu khác. Cách viết ngắn nhất là parameter property, constructor(readonly id: string) {}, vừa khai báo vừa gán trường chỉ trong một bước. Trang lớp (class) trình bày về trường và hàm khởi tạo nói chung.
Readonly<T>: mọi thuộc tính cùng lúc
Readonly<T> là một utility type đánh dấu mọi thuộc tính của T là readonly. Nó hữu ích cho những giá trị bạn truyền đi khắp nơi nhưng không nên thay đổi, như state của ứng dụng:
Chữ ký hàm cho người đọc biết addItem trả về một state mới thay vì sửa state cũ, và trình biên dịch buộc hàm phải giữ đúng điều đó. Readonly<T> được định nghĩa là mapped type { readonly [P in keyof T]: T[P] }.
Mảng chỉ đọc: readonly T[] và ReadonlyArray<T>
readonly number[] và ReadonlyArray<number> là cùng một kiểu. Chúng loại bỏ mọi phương thức làm thay đổi mảng (push, pop, shift, splice, sort, reverse, fill...) và cấm gán theo chỉ số. Các phương thức không làm thay đổi mảng vẫn còn và trả về mảng thông thường:
Nhận readonly T[] làm tham số là lời cam kết với nơi gọi rằng bạn sẽ không sửa mảng của họ. Chiều ngược lại mới là chỗ nhiều người gặp khó: không thể truyền mảng chỉ đọc vào một hàm nhận T[] thông thường, vì hàm đó có thể làm thay đổi mảng.
index.ts(7,17): error TS4104: The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
Cách sửa là đổi sum để nhận readonly number[], vì nó không thay đổi gì cả. Hàm chỉ đọc mảng thì luôn nên nhận kiểu readonly; khi đó nó chấp nhận cả hai loại. Nếu hàm không phải của bạn, hãy truyền một bản sao: sum([...prices]).
ReadonlyMap và ReadonlySet
Map và set cũng có phiên bản chỉ đọc. ReadonlyMap<K, V> có get, has, size, forEach và các iterator nhưng không có set, delete hay clear; ReadonlySet<T> không có add, delete hay clear:
Một lớp thường giữ một Map private có thể thay đổi và công khai nó qua một getter có kiểu ReadonlyMap, để code bên ngoài đọc được dữ liệu nhưng không thay đổi được nó qua tham chiếu đó.
readonly chỉ ở mức nông
readonly và Readonly<T> chỉ bảo vệ bản thân thuộc tính, không bảo vệ object hay mảng mà nó trỏ tới:
DeepReadonly<T> tự áp dụng lại cho mọi kiểu object lồng bên trong, và vì một mapped type trên kiểu mảng tạo ra mảng chỉ đọc, members trở thành readonly string[]. Đây vẫn chỉ là cam kết ở cấp độ kiểu, không phải bảo vệ lúc chạy.
Chỉ có tác dụng lúc biên dịch: thay đổi qua tham chiếu khác
Một kiểu readonly kiểm soát những gì một tham chiếu được phép làm. Một tham chiếu khác tới cùng object, có kiểu không có readonly, vẫn có thể thay đổi nó, và TypeScript thậm chí cho phép gán một kiểu readonly cho một kiểu có thể thay đổi:
Phép gán mutable = settings biên dịch được vì TypeScript không tính đến các thuộc tính readonly khi kiểm tra hai kiểu object có tương thích hay không; TypeScript handbook nói rõ điều này, và lưu ý rằng vì thế thuộc tính readonly có thể bị thay đổi qua aliasing. Mảng chỉ đọc thì khác: lỗi TS4104 ở trên chính là phép kiểm tra đó. Object.freeze thực sự ngăn thay đổi lúc chạy: code được sinh ra chạy ở strict mode, nơi ghi vào một thuộc tính đã bị đóng băng sẽ ném ra TypeError. Giống readonly, Object.freeze cũng chỉ ở mức nông.
readonly, const, as const và Object.freeze
as const trên một literal biến mọi thuộc tính thành readonly ở mọi cấp và giữ nguyên literal type, thường là cách dễ nhất để có một giá trị chỉ đọc ở mọi cấp:
const theme = { mode: "dark", sizes: [12, 14] } as const;
// { readonly mode: "dark"; readonly sizes: readonly [12, 14] }
| Áp dụng cho | Áp dụng sâu? | Tác dụng lúc chạy | Ví dụ | |
|---|---|---|---|---|
const | một biến | không | không thể gán lại biến | const user = {...} |
readonly | một thuộc tính hoặc kiểu mảng | không | không có | readonly id: string |
Readonly<T> | mọi thuộc tính của một kiểu | không | không có | Readonly<State> |
as const | một biểu thức literal | có | không có | { ... } as const |
Object.freeze | một giá trị object | không | phép ghi thất bại (ném lỗi ở strict mode) | Object.freeze(obj) |
const và readonly trả lời hai câu hỏi khác nhau: const ngăn tên biến trỏ sang chỗ khác, readonly ngăn một thuộc tính bị thay đổi. Thuộc tính của một object const vẫn có thể bị gán lại, trừ khi chúng là readonly.
Câu hỏi thường gặp
readonly trong TypeScript dùng để làm gì?
readonly đánh dấu một thuộc tính có thể được đặt khi tạo object (hoặc trong hàm khởi tạo của lớp) nhưng không thể gán lại sau đó. Gán cho nó về sau là lỗi biên dịch, TS2540. Đây chỉ là kiểm tra kiểu: JavaScript được sinh ra không có cơ chế bảo vệ nào.
readonly và const trong TypeScript khác nhau thế nào?
const nói về biến: tên biến không thể trỏ sang giá trị khác, nhưng object mà nó giữ vẫn có thể bị thay đổi. readonly nói về thuộc tính: thuộc tính đó không thể bị gán lại. const user = { name: "Ada" } vẫn cho phép user.name = "x"; một thuộc tính readonly name thì không.
Làm sao tạo mảng chỉ đọc trong TypeScript?
Chú thích kiểu cho nó là readonly T[] hoặc ReadonlyArray<T> (hai kiểu này giống nhau). Các phương thức làm thay đổi mảng như push, pop, sort và splice biến mất khỏi kiểu, và gán theo chỉ số là lỗi. Các phương thức không làm thay đổi mảng như map, filter và slice vẫn hoạt động và trả về mảng thông thường.
Readonly trong TypeScript có áp dụng sâu không?
Không. Readonly<T> và readonly chỉ bảo vệ các thuộc tính ở cấp ngoài cùng; object và mảng lồng bên trong vẫn có thể bị thay đổi. Hãy dùng as const trên một literal, hoặc viết kiểu đệ quy DeepReadonly<T>, để được bảo vệ sâu ở cấp độ kiểu.
readonly có ngăn thay đổi lúc chạy không?
Không. Kiểu bị xóa khi biên dịch, nên một thuộc tính readonly là thuộc tính bình thường lúc chạy, và code giữ một tham chiếu có thể thay đổi tới cùng object (hoặc JavaScript thuần) vẫn có thể sửa nó. Dùng Object.freeze khi bạn cần bảo vệ lúc chạy; TypeScript gán cho kết quả của nó kiểu Readonly<T>.