Menu

readonly trong TypeScript: thuộc tính, Readonly<T> và mảng

Modifier readonly và utility type Readonly<T> ngăn code gán lại thuộc tính. Tìm hiểu thuộc tính readonly và trường của lớp, Readonly<T>, mảng chỉ đọc (readonly T[] và ReadonlyArray), ReadonlyMap và ReadonlySet, vì sao readonly chỉ ở mức nông và chỉ có tác dụng lúc biên dịch, và so sánh với Object.freeze và as const.

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.

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ạyVí dụ
constmột biếnkhôngkhông thể gán lại biếnconst user = {...}
readonlymột thuộc tính hoặc kiểu mảngkhôngkhông córeadonly id: string
Readonly<T>mọi thuộc tính của một kiểukhôngkhông cóReadonly<State>
as constmột biểu thức literalcókhông có{ ... } as const
Object.freezemột giá trị objectkhôngphé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>.

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