Prywatne pola w TypeScriptie a składnia `#`: prywatność w czasie kompilacji czy wykonywania
Porównuje modyfikator `private` w TypeScriptie, który jest sprawdzany w czasie kompilacji, z polami oznaczonymi `#` w ECMAScriptie, które są kontrolowane w czasie wykonywania, aby pomóc w doborze odpowiedniej strategii zamykania danych dla baz kodu z 2026 roku.
Bardzo wiele problemów związanych z prywatnością w projektach TypeScript wynika z mylenia dwóch modeli inkapsulacji, które w rzeczywistości nie działają w ten sam sposób: słowa kluczowego private dostępnego tylko w kompilatorze oraz składni pola # z ECMAScript, która jest egzekwowana w czasie wykonywania. Zespoły często bez głębszego zastanowienia decydują się na jeden z tych podejść, wdrażają go, a później napotykają sytuacje, w których ta decyzja powoduje nieoczekiwane problemy.
Słowo kluczowe private w TypeScript nie zapewnia żadnej ochrony po uruchomieniu kodu. Kompilator sprawdza dostępność elementów podczas pisania i budowania projektu, ale generowany JavaScript zamienia każdy „prywatny” element na zwykłą, publiczną właściwość. Wszystko, co korzysta z skompilowanego wyniku, może całkowicie obejść ograniczenia dostępu.
Pola ECMAScript # przyjmują inne podejście, zapewniając prawdziwą prywatność. Sam silnik wykonawczy egzekwuje te ograniczenia, przechowując dane pól w wewnętrznym WeakMap, dzięki czemu kod zewnętrzny nie ma możliwości do nich dostępu. Ta gwarancja chroni przed przypadkowym nadużyciem i zapewnia bezpieczeństwo wrażliwych wartości nawet w środowiskach, którym nie ufasz w pełni.
Wybór między tymi dwoma podejściami decyduje o tym, czy twoja inkapsulacja faktycznie będzie skuteczna po wprowadzeniu kodu do produkcji, dlatego tak ważne jest prawidłowe jej zastosowanie.
Główne wnioski
- Modyfikator
privatew TypeScript zostaje usunięty podczas kompilacji, pozostawiając zwykłe, swobodnie dostępne właściwości JavaScript. Natomiast pola ECMAScript#wykorzystują przechowywanie oparte na WeakMap, które zachowuje prywatność nawet po transpilacji.
private, gdy potrzebujesz bezpieczeństwa na poziomie typów w bazie kodu, gdzie TypeScript jest jedynym narzędziem wykorzystującym ten kod i wystarczające są sprawdzenia w czasie kompilacji. Użyj #, gdy publikujesz bibliotekę, pracujesz z dynamicznymi importami lub chcesz chronić wrażliwe wartości przed inspekcją w czasie wykonywania.private na # zmienia wygląd publicznego API i może uszkodzić narzędzia oparte na refleksji. Bez udokumentowanej konwencji bazy kodu zawierają niespójne połączenia obu podejść.private po prostu dlatego, że przypomina ono wzorce z języków takich jak Java, a potem są zaskoczone, gdy narzędzia używające wyłącznie JavaScript ignorują ten kontrakt całkowicie. Powstałe w ten sposób błędy są zazwyczaj niewidoczne, ale trudne do wykrycia.Rozumienie modyfikatora private w TypeScript: tylko w czasie kompilacji
Słowo kluczowe private w TypeScript jest czysto konstrukcją systemu typów. Zapobiega ono nieuprawnionemu dostępowi podczas rozwoju, ale po skompilowaniu powstały JavaScript zawiera zwykłe, niezabezpieczone właściwości. W praktyce funkcje private służą bardziej jako narzędzie pomocnicze do dokumentacji niż jako rzeczywista bariera bezpieczeństwa.
Gdy klasa z polem private zostanie skompilowana do zwykłego JavaScriptu, modyfikator ten całkowicie znika, a pole staje się zwykłą właściwością, którą każdy użytkownik może bezpośrednio odczytać lub przepisać. Stanowi to poważny problem w trzech sytuacjach: wysyłaniu pakietów do npm, dynamicznym importowaniu modułów od third-party lub integracji z narzędziami opartymi na refleksji, takimi jak serializatory i ORM.
Prostota jest główną zaletą mechanizmu private. Programiści przychodzący z języków takich jak Java czy C# szybko go opanowują. Edytorzy ukrywają elementy prywatne przed sugestiami automatycznego uzupełniania, narzędzia do refaktoryzacji respektują zamierzoną widoczność, a kompilator wykrywa przypadkowe ujawnienie tych elementów podczas rozwoju i przeglądania kodu.
Problemy pojawiają się, gdy założenia dotyczące środowiska wykonywania okazują się błędne. Wyobraźmy sobie zespół tworzący wewnętrzną panel kontrolny, zakładając, że każdy użytkownik ich kodu korzysta z TypeScript. Kilka miesięcy później usługa napisana w Pythonie ładuje skompilowany plik JavaScript i zaczyna bezpośrednio modyfikować tokeny sesji. Rzekoma granica prywatności nigdy nie istniała poza kompilatorem, więc w ogóle nie stanowiła żadnej bariery.
Ten model funkcjonuje dobrze, dopóki kontrolujesz cały łańcuch zależności i TypeScript jest stosowany od początku do końca. Gdy tylko twój kod przekroczy granicę między językami lub zostanie opublikowany w miejscu publicznym, atrybut private przestaje być gwarancją i staje się zaledwie sugestią.
Prywatne pola ECMAScript (#): Bezwzględna ochrona prywatności zapewniona przez środowisko wykonywania
Pola poprzedzone # to wbudowana cecha JavaScripta, która umożliwia tworzenie właściwości rzeczywiście niedostępnych z zewnątrz klasy. Silnik przechowuje je w wewnętrznym WeakMap, ukrytym przed refleksją oraz przed dowolnym kodem zewnętrznym próbującym do nich uzyskać dostęp. Gdy celujemy w ES2022 lub nowsze wersje, TypeScript zachowuje składnię # w swoim wyjściu bez zmian.
Podczas kompilacji dla nowoczesnych celów generowany JavaScript zachowuje składnię # w niezmienionej formie, zamiast przekształcać ją w zwykłą właściwość. Encapsulacja jest tutaj gwarantowana przez sam silnik: próba dostępu do pola takiego jak wallet.#balance z zewnątrz klasy powoduje błąd składniowy w trybie strict. Nawet wywołanie Object.keys(wallet) zwraca pusty tablicę, ponieważ pola prywatne znajdują się całkowicie poza normalnym mechanizmem wyliczania właściwości.
Kompromisem związanym z polami # jest kompatybilność. Dostosowywanie się do starszych środowisk, takich jak ES5 czy ES2015, zmusza TypeScript do generowania polyfilli opartych na WeakMap, co zwiększa wagę pliku i dodatkowe obciążenie przy każdym dostępie — co stanowi poważny problem dla bibliotek przeznaczonych do środowisk przeglądarek wymagających wysokiej wydajności.
Istnieje również koszt związany z doświadczeniem programisty. Edytorzy nie mogą oferować funkcji autodopasowywania dla pól # spoza ich klasy, a niektóre narzędzia do debugowania domyślnie ukrywają je przed inspektorami obiektów. Narzędzia do serializacji, takie jak JSON.stringify, w tle pomijają również pola prywatne, co może zaskoczyć programistów oczekujących pełnego obrazu stanu obiektu.
Pola prywatne mają największe znaczenie w trzech przypadkach: ochronie kluczy kryptograficznych lub tokenów dostępu, uniemożliwieniu użytkownikom API modyfikowania wewnętrznych stałych oraz uruchamianiu kodu w środowiskach, gdzie nie można w pełni zaufać temu, co jest uruchamiane równocześnie z nim. W takich sytuacjach silna gwarancja działania w czasie rzeczywistym jest cenniejsza niż wygoda, której się rezygnuje.
Porównanie side-by-side: kiedy każde podejście jest lepsze
Wybór między private a # zależy ostatecznie od granic zaufania oraz potrzeb narzędziowych — żadne z nich nie jest odpowiednią domyślną opcją we wszystkich sytuacjach. Dlatego zespoły powinny uzgodnić wyraźne zasady, zamiast po prostu stosować to, co wydaje się najbardziej znajome.
Użyj private w TypeScriptie, gdy:
- Budujesz aplikację wewnętrzną, w której każdy użytkownik to kod napisany w TypeScriptie przy użyciu ścisłych ustawień kompilatora.
# znacząco zwiększyłyby rozmiar pliku.Zastosuj pola ECMAScript #, gdy:
- Publikujesz pakiet w npm i nie możesz zagwarantować, że wszyscy użytkownicy będą przestrzegać konwencji typowych tylko dla TypeScript.
- Zachowujesz wrażliwe dane, takie jak tokeny autoryzacji, klucze szyfrowania lub informacje płatnicze, które nie powinny być dostępne do inspekcji.
- Budujesz system pluginów, w którym niezaufany kod third-party korzysta z tego samego środowiska uruchomieniowego co twój własny.
- Projektujesz framework lub SDK, w którym umowa API musi być egzekwowana strukturalnie, a nie tylko udokumentowana.
Konflikty pojawiają się, gdy projekt wymaga jednocześnie obsługi refleksji oraz prawdziwej prywatności w czasie wykonywania. Typowym przykładem jest sytuacja, gdy ORM próbuje przeliczyć wszystkie pola w celu utworzenia mapowania bazy danych, ale pola oznaczone # po prostu nie pojawiają się w tym przeliczeniu. Aby temu zaradzić, zazwyczaj trzeba dodać wyraźne metody getter lub dekoratory metadanych, co zwiększa złożoność, której wiele zespołów wolałoby uniknąć.
Testowanie wprowadza kolejny problem. Dzięki modifikatorowi private w TypeScript pliki testowe w tym samym projekcie nadal mogą uzyskać dostęp do stanu wewnętrznego za pomocą stwierdzeń typowych. W przypadku pól oznaczonych # zazwyczaj trzeba wyodrębnić testowalne zachowanie do osobnych metod lub skorzystać z iniekcji zależności. Zespoły przyzwyczajone do ingerencji w prywatne elementy kodu podczas testowania często uważają tę dodatkową trudność za irytującą.
Biorąc pod uwagę sytuację z 2026 roku, pola # stają się coraz częstsze w bibliotekach wymagających wysokiego poziomu bezpieczeństwa, podczas gdy modifikatory private pozostają standardem w kodzie aplikacji wewnętrznych. TypeScript 5.7 traktuje oba podejścia jako w pełni wspierane funkcje pierwszej klasy, obejmujące mechanizmy inferencji i sprawdzania błędów, więc decyzja dotyczy raczej architektury niż możliwości kompilatora.
Kod z praktyki: wdrożenie obu wzorców
Często zdarza się, że jeden kod produkcyjny wykorzystuje oba te wzorce do różnych celów jednocześnie. Najważniejsze jest zachowanie spójności w obrębie określonych granic: używaj private dla zwykłych szczegółów implementacji, a # przeznacz dla pól związanych z bezpieczeństwem.
Rozważmy klasę, która przechowuje pole requestCache oznaczone jako private oraz pole #authToken oznaczone jako hard-private. Pole requestCache pozostaje private, ponieważ narzędzia do testowania i debugowania mogą z nich korzystać, a narzędzia seryjalizacji mogą je wykorzystać w razie potrzeby. Tymczasem #authToken ma status hard-private, ponieważ jego ujawnienie w czasie wykonywania programu mogłoby stworzyć rzeczywistą lukę bezpieczeństwa.
Mieszanie tych dwóch podejść ma sens, gdy pola charakteryzują się różnym poziomem ryzyka. Takie elementy jak wartości konfiguracji i pamięci cache to wewnętrzne szczegóły, w których pewna elastyczność jest przydatna. Z kolei dane uwierzytelniające i klucze kryptograficzne wymagają silniejszych gwarancji podczas wykonywania kodu.
Kolejnym praktycznym przykładem jest maszyna stanowa, która przechowuje swoje reguły przejść jako pola private, ale ukrywa swój rzeczywisty stan za znacznikiem #. Maszyna ta udostępnia stan jedynie poprzez metodę getState(), pozostawiając podstawowe pole #currentState niedostępne, co zapobiega bezpośredniej modyfikacji tego pola przez kod z zewnątrz. Mapa validTransitions pozostaje polem private, ponieważ zestawy testowe mogą nadal potrzebować sprawdzenia zbioru reguł w celu przeanalizowania przypadków krawędziowych.
Ta konwencja sprawdza się doskonale w praktyce. Baza kodu składająca się na przykład z 50 klas może używać pól # w około 10 klasach związanych z autoryzacją, natomiast we wszystkich pozostałych miejscach polegać na atrybutach private. Obecność znaku # w kodzie stanowi więc wyraźny sygnał, że dotarliśmy do obszaru wrażliwego pod względem bezpieczeństwa.
Strategie migracji i konwencje zespołu
Zmiana klasy z atrybutów private na # stanowi zmianę rozrywającą pod względem jej publicznego interfejsu. Wszystkie narzędzia, które polegają na wyliczaniu właściwości, przestaną prawidłowo funkcjonować, dlatego taka migracja musi być starannie zaplanowana i wdrażana stopniowo, przy koordynacji między wszystkimi zespołami korzystającymi z danego kodu.
Migracja klasy z atrybutów private na p поля # zazwyczaj przebiega według przewidywalnej sekwencji:
- Przeglądaj, które pola faktycznie wymagają ochrony prywatności realizowanej w czasie wykonywania, a które potrzebują jedynie sprawdzeń widoczności na poziomie kompilatora.
- Odłóż publikację nowej wersji, która dokumentowałaby zmiany w publicznym interfejsie API.
- Zmień pola krytyczne pod względem bezpieczeństwa na składnię
#w dedykowanej gałęzi funkcji. - Zmień testy wewnętrzne tak, aby już nie polegały na bezpośrednim dostępie do pól.
- Potwierdź, że biblioteki serializacji oraz ORM nadal działają poprawnie przy zaktualizowanej strukturze pól.
- Opublikuj wersję z szczegółowymi notatkami wyjaśniającymi dokładnie, co się zepsuło i dlaczego.
Bazy kodu wewnętrzne mają tu łatwiejszą drogę. Ponieważ nie trzeba się martwić żadnym zewnętrznym użytkownikiem, zespoły mogą wprowadzać zmiany stopniowo, bez konieczności śledzenia wersjonowania semantycznego. Prawdziwym wyzwaniem staje się koordynacja: programiści potrzebują wspólnego zrozumienia, kiedy # jest obowiązkowe, a kiedy private nadal jest do przyjęcia.
Praktyczna konwencja, którą przyjmuje wiele zespołów, wygląda następująco:
- Zarezerwuj
#dla tokenów autoryzacyjnych, kluczy szyfrujących, danych dostępu do bazy danych oraz informacji identyfikujących osobę. - Używaj
privatedla warstw cache’owania, stanu konfiguracji, wewnętrznych maszyn stanowych oraz wartości wyliczanych lub pochodnych. - Zapisz uzasadnienie w liście kontrolnym przeglądu kodu oraz w dokumentacji wprowadzającej, aby nowi pracownicy mogli szybko opanować tę zasadę.
private zamiast #.Organizacje, które przechowują zarówno TypeScript, jak i starszy JavaScript w tym samym repozytorium, potrzebują nieco innego zestawu zasad. W monorepo łączącym usługi TypeScript z starszymi modułami JavaScript, stosowanie pól # we wszystkich miejscach nie jest praktyczne ze względu na koszt transpilacji, jaki generuje dla kodu, który tego nie wymaga. W takim ustawieniu granica zazwyczaj leży na poziomie repozytorium lub pakietu: nowo pisane moduły TypeScript używają # do przechowywania danych wrażliwych, podczas gdy starszy JavaScript pozostaje bez zmian aż do jego ostatecznej przepisania.
Strategia hybrydowa o takim charakterze występuje w systemach rozproszonych, które łączą ścisłą ochronę prywatności w czasie wykonywania w niektórych usługach z umowami na poziomie typów w innych, przy czym niektóre komponenty zapewniają ścisłe zamknięcie, a inne polegają wyłącznie na sprawdzaniach przez kompilatora.
Większość problemów z migracją wynika z traktowania tego mechanizmu jako czysto mechanicznego narzędzia do wyszukiwania i zastępowania. Zamienna private na # bez uprzedniego sprawdzenia każdego użytkownika tego pola prowadzi do ukrytych awarii. Weźmy na przykład narzędzie do logowania oparte na refleksji, które przegląda właściwości obiektu w celu wygenerowania informacji diagnostycznych — gdy te właściwości stają się polami typu #, znikają z tej listy, a narzędzie logowania bez żadnych komunikatów traci możliwość obserwacji stanu obiektu. Prawidłowym rozwiązaniem jest udostępnianie potrzebnych danych za pomocą wyraźnych metod getter lub strukturyzowanej interfejsu do logowania, zamiast polegać na refleksji w celu odsłonięcia prywatnych elementów wewnętrznych.
Często zadawane pytania
Czy mogę łączyć modyfikatory private z polami typu # w tej samej klasie?
Tak. TypeScript 5.7 i nowsze wersje pozwalają, aby oba te mechanizmy współistniały w ramach jednej definicji klasy. Powszechnym wzorcem jest używanie słowa kluczowego private dla szczegółów implementacji, do których testy lub narzędzia do debugowania mogą nadal potrzebować dostępu, natomiast rezerwowanie # dla nielicznych pól, które muszą być chronione przed jakąkolwiek inspekcją w czasie wykonywania. Kompilator traktuje je jako dwa oddzielne, lecz kompatybilne systemy widoczności.
Czy pola oznaczone # działają w starszych środowiskach JavaScript, takich jak IE11?
Nie bezpośrednio. Gdy celem kompilacji jest standard ES5 lub ES2015, kompilator TypeScript ucieka się do wykorzystywania polyfilli opartych na WeakMap, aby symulować zachowanie pól #, co zwiększa rozmiar kodu oraz obciąża wydajność w czasie działania. Jeśli nadal musisz obsługiwać starsze przeglądarki, lepiej pozostać przy modyfikatorach private i zaakceptować egzekwowanie zasad tylko w czasie kompilacji, zamiast ponosić koszty związane z polyfillami.
Co dzieje się z polami # podczas serializacji do JSON?
JSON.stringify oraz podobne narzędzia do serializacji po prostu nie potrafią dostrzec pól #, więc każdy obiekt je zawierający zostanie zserializowany bez tych właściwości. Jeśli potrzebujesz, aby część tego prywatnego stanu była widoczna w wyniku, musisz ją celowo ujawnić — albo za pomocą metody getter, albo poprzez zdefiniowanie własnej implementacji toJSON, która precyzyjnie określa, co ma zostać włączone.
Czy podklasy mogą uzyskać dostęp do pól # klasy nadrzędnej?
Nie. Prywatne pola ECMAScript należą wyłącznie do klasy, w której zostały zadeklarowane, i ta granica jest bezwzględna — podklasa nie ma możliwości odczytu ani modyfikacji pól # klasy nadrzędnej, nawet pośrednio za pomocą metod chronionych lub publicznych. Jest to istotna różnica w porównaniu z modyfikatorami private, gdzie kompilator TypeScript czasami zezwala na dostęp podklasy poprzez wyraźne stwierdzenia typu.
Czy powinienem przenieść istniejące bazy kodu z pól private na pola #?
Tylko wtedy, gdy istnieje konkretna potrzeba — rzeczywiste zagrożenie dla bezpieczeństwa lub prawdziwy wymóg ochrony prywatności w czasie wykonywania programu. Ponieważ migracja stanowi istotną zmianę wpływającą na narzędzia, zachowanie serializacji oraz projekt testów, nie jest to coś, co można robić lekkomyślnie. Dla większości aplikacji wewnętrznych modyfikatory private już zapewniają wystarczającą inkapsulację, więc koszt migracji nie jest uzasadniony. Najpierw należy zająć się przeniesieniem bibliotek współdzielonych, interfejsów API dostępnych publicznie lub modułów obsługujących dane wrażliwe.
Wybór odpowiedniego modelu ochrony prywatności dla twojej bazy kodu w 2026 roku
Wybór pomiędzy atrybutem private w TypeScript a atrybutem # w ECMAScript nie jest kwestią arbitralnych preferencji. Ta decyzja wpływa na to, czy gwarancje związane z zamknięciem klas przetrwają po przetworzeniu przez kompilator, na to, w jaki sposób kod zewnętrzny może współpracować z twoimi klasami, oraz na to, jak zostanie przekazana intencja architektoniczna osobom, które później będą utrzymywać ten kod.
Używaj atrybutu private, gdy kontrolujesz cały graf zależności i stawiasz na narzędzia dla programistów ponad ścisłe gwarancje w czasie wykonywania. Zastosuj atrybut #, gdy twój kod działa w niezaufanych środowiskach lub przetwarza dane, które w żadnym wypadku nie mogą zostać ujawnione poprzez refleksję. Większość rzeczywistych baz kodu ostatecznie wymaga połączenia obu rozwiązań, stosowanych celowo w zależności od tego, co reprezentuje każdy z tych atrybutów.
Błędne podjęcie tej decyzji często ma konsekwencje w środowisku produkcyjnym: niezmienniki zostają naruszone wskutek przypadkowych modyfikacji, dane uwierzytelniające wyciekają przez infrastrukturę logowania, a zestawy testowe tracą możliwość weryfikacji stanu wewnętrznego. Wszystkim tym można zapobiec dzięki jasnym konwencjom i udokumentowanym zasadom architektonicznym.
Zespoły tworzące narzędzia wewnętrzne mogą zazwyczaj polegać na modyfikatorach private oraz korzystać z rozwiniętych narzędzi stworzonych wokół nich. Zespoły dostarczające biblioteki publiczne lub pracujące w obszarach wymagających szczególnej ostrożności potrzebują gwarancji działania w czasie rzeczywistym, które zapewniają pola #. Obie te metody są w równym stopniu obsługiwane we współczesnym ekosystemie TypeScript – właściwy wybór zależy od modelu zagrożeń oraz stopnia zaufania, jakim darzysz użytkowników swojej API.
Literatura pokrewna
- Co właściwie robi i nie robi native support TypeScript-u w Node.js — Ten artykuł wyjaśnia, jak Node.js uruchamia pliki .ts w sposób natywny poprzez usuwanie informacji typów, dlaczego pomija sprawdzanie typów oraz kiedy nadal potrzebny jest prawdziwy krok budowania.
- Wzory projektowe React: od klasycznej programacji obiektowej do nowoczesnych hooków — Wyjaśnia, jak klasyczne wzory oprogramowania takie jak Singleton, Factory i Observer znajdują zastosowanie w React, a także wzory specyficzne dla Reacta, takie jak HOC-y, hooki i składane komponenty.