readonly oznacza właściwość, którą można ustawić raz, przy tworzeniu obiektu, i nigdy więcej nie przypisać. Readonly<T> stosuje to do każdej właściwości typu, a readonly T[] robi to samo dla tablic:
Ostatnia linia pokazuje najważniejszy fakt o readonly: sprawdza go kompilator, ale nie jest egzekwowany w czasie działania. Przypisanie było błędem kompilacji (tu wyciszonym przez @ts-expect-error), a mimo to wygenerowany JavaScript je wykonał. Bez wyciszenia plik by się nie skompilował i właśnie tam readonly spełnia swoje zadanie.
Właściwości readonly
Postaw readonly przed nazwą właściwości w interfejsie, literale typu lub klasie. Właściwość można zainicjalizować, ale nie ponownie przypisać:
W klasie pole readonly można przypisać w jego deklaracji albo w konstruktorze i nigdzie indziej. Najkrótsza forma to parameter property, constructor(readonly id: string) {}, która deklaruje i przypisuje pole w jednym kroku. Strona o klasach omawia pola i konstruktory ogólnie.
Readonly<T>: wszystkie właściwości naraz
Readonly<T> to typ narzędziowy, który oznacza każdą właściwość T jako readonly. Przydaje się dla wartości, które przekazujesz dalej, ale których nie należy zmieniać, na przykład stanu aplikacji:
Sygnatura funkcji mówi czytelnikowi, że addItem zwraca nowy stan zamiast zmieniać stary, a kompilator pilnuje, żeby funkcja się tego trzymała. Readonly<T> jest zdefiniowany jako typ mapowany { readonly [P in keyof T]: T[P] }.
Tablice tylko do odczytu: readonly T[] i ReadonlyArray<T>
readonly number[] i ReadonlyArray<number> to ten sam typ. Usuwają każdą metodę mutującą (push, pop, shift, splice, sort, reverse, fill...) i zabraniają przypisania przez indeks. Metody niemutujące zostają i zwracają zwykłe tablice:
Przyjmowanie readonly T[] jako parametru to obietnica dla wywołujących, że nie zmodyfikujesz ich tablicy. W drugą stronę ludzie często utykają: tablicy readonly nie można przekazać do funkcji przyjmującej zwykłe T[], bo ta funkcja mogłaby ją zmienić.
index.ts(7,17): error TS4104: The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
Rozwiązanie to zmienić sum tak, by przyjmowała readonly number[], bo niczego nie mutuje. Funkcje, które tylko czytają tablicę, powinny zawsze przyjmować typ readonly; wtedy akceptują oba rodzaje. Jeśli funkcja nie jest twoja, przekaż kopię: sum([...prices]).
ReadonlyMap i ReadonlySet
Mapy i zbiory też mają wersje tylko do odczytu. ReadonlyMap<K, V> ma get, has, size, forEach i iteratory, ale nie ma set, delete ani clear; ReadonlySet<T> nie ma add, delete ani clear:
Klasa często trzyma prywatną mutowalną Map i udostępnia ją przez getter o typie ReadonlyMap, więc kod z zewnątrz może czytać dane, ale nie może ich zmienić przez tę referencję.
readonly jest płytkie
readonly i Readonly<T> chronią tylko samą właściwość, a nie obiekt lub tablicę, na którą wskazuje:
DeepReadonly<T> stosuje się do każdego zagnieżdżonego typu obiektowego, a ponieważ typ mapowany na typie tablicy daje tablicę readonly, members staje się readonly string[]. To nadal obietnica na poziomie typów, a nie ochrona w czasie działania.
Tylko podczas kompilacji: mutacja przez inną referencję
Typ readonly kontroluje, co może zrobić jedna referencja. Inna referencja do tego samego obiektu, otypowana bez readonly, może go zmienić, a TypeScript pozwala nawet przypisać typ readonly do mutowalnego:
Przypisanie mutable = settings się kompiluje, bo TypeScript nie bierze pod uwagę właściwości readonly, gdy sprawdza, czy dwa typy obiektowe są zgodne; handbook TypeScriptu mówi to wprost i zauważa, że właściwości readonly mogą się przez to zmieniać przez aliasy. Z tablicami readonly jest inaczej: błąd TS4104 wyżej to właśnie takie sprawdzenie. Object.freeze naprawdę blokuje zmiany w czasie działania: wygenerowany kod działa w trybie strict, w którym zapis do zamrożonej właściwości rzuca TypeError. Tak jak readonly, Object.freeze jest płytkie.
readonly, const, as const i Object.freeze
as const na literale czyni każdą właściwość readonly na każdym poziomie i zachowuje typy literałowe, co często jest najprostszym sposobem na głęboko readonly wartość:
const theme = { mode: "dark", sizes: [12, 14] } as const;
// { readonly mode: "dark"; readonly sizes: readonly [12, 14] }
| Dotyczy | Głęboko? | Efekt w czasie działania | Przykład | |
|---|---|---|---|---|
const | wiązania zmiennej | nie | zmiennej nie można ponownie przypisać | const user = {...} |
readonly | jednej właściwości lub typu tablicy | nie | brak | readonly id: string |
Readonly<T> | każdej właściwości typu | nie | brak | Readonly<State> |
as const | wyrażenia literałowego | tak | brak | { ... } as const |
Object.freeze | wartości obiektu | nie | zapisy się nie udają (rzucają w trybie strict) | Object.freeze(obj) |
const i readonly odpowiadają na różne pytania: const nie pozwala nazwie wskazywać gdzie indziej, readonly nie pozwala zmienić właściwości. Właściwości obiektu const nadal można ponownie przypisać, chyba że są readonly.
Najczęściej zadawane pytania
Co robi readonly w TypeScript?
readonly oznacza właściwość, którą można ustawić przy tworzeniu obiektu (lub w konstruktorze klasy), ale nie można jej potem ponownie przypisać. Późniejsze przypisanie to błąd kompilacji, TS2540. To tylko sprawdzenie typów: wygenerowany JavaScript nie zawiera żadnej ochrony.
Czym różni się readonly od const w TypeScript?
const dotyczy zmiennej: nazwy nie można skierować na inną wartość, ale obiekt, który przechowuje, nadal można zmieniać. readonly dotyczy właściwości: tej właściwości nie można ponownie przypisać. const user = { name: "Ada" } nadal pozwala na user.name = "x"; właściwość readonly name nie pozwala.
Jak zrobić tablicę tylko do odczytu w TypeScript?
Oznacz ją jako readonly T[] albo ReadonlyArray<T> (to ten sam typ). Metody mutujące, takie jak push, pop, sort i splice, znikają z typu, a przypisanie przez indeks to błąd. Metody niemutujące, jak map, filter i slice, nadal działają i zwracają zwykłe tablice.
Czy Readonly w TypeScript działa głęboko?
Nie. Readonly<T> i readonly chronią tylko właściwości najwyższego poziomu; zagnieżdżone obiekty i tablice w środku nadal można zmieniać. Dla głębokiej ochrony na poziomie typów użyj as const na literale albo napisz rekurencyjny typ DeepReadonly<T>.
Czy readonly zapobiega zmianom w czasie działania?
Nie. Typy są usuwane, więc właściwość readonly jest w czasie działania zwykłą właściwością, a kod z mutowalną referencją do tego samego obiektu (albo zwykły JavaScript) nadal może ją zmienić. Użyj Object.freeze, gdy potrzebujesz ochrony w czasie działania; TypeScript typuje jego wynik jako Readonly<T>.