В TypeScript три модификатора доступа для членов класса: public (по умолчанию), protected и private. Они определяют, где можно использовать член, и компилятор сообщает о любом обращении не из того места.
Последняя строка это ошибка компиляции, но посмотрите, что она напечатала, когда её всё-таки запустили: 123-45-6789. Это самый важный факт на этой странице, и раздел о private его объясняет.
Что разрешает каждый модификатор
| Модификатор | Внутри класса | В подклассе | Снаружи | Соблюдается во время выполнения |
|---|---|---|---|---|
public (по умолчанию) | да | да | да | соблюдать нечего |
protected | да | да | нет | нет |
private | да | нет | нет | нет |
#name (JavaScript) | да | нет | нет | да |
readonly | чтение, а запись в конструкторе | чтение | чтение | нет |
Модификаторы работают для полей, методов, геттеров и сеттеров, конструкторов и свойств-параметров (constructor(private id: string)). Писать public необязательно, и многие кодовые базы его опускают.
private это проверка времени компиляции
TypeScript стирает private вместе с остальными типами. В скомпилированном классе это обычное свойство, поэтому его видит всё, что не проходит через проверку типов: вызывающий код на обычном JavaScript, JSON.stringify, Object.keys и даже собственная запись TypeScript через квадратные скобки, которая разрешена как намеренная лазейка.
Для задачи, ради которой существует private, этого достаточно: сообщить другим разработчикам (и вашему редактору), что член это деталь реализации. Это не граница безопасности, и поле private окажется в логах и ответах JSON, если вы сами его не уберёте.
Поля #private: соблюдаются во время выполнения
В JavaScript есть собственные приватные поля, которые пишутся через #. Их соблюдает движок: вне класса obj.#field даже не является корректным синтаксисом, а поле не показывается в Object.keys, JSON.stringify или console.log.
Запись s.#token вне класса это ошибка TS18013 (Property '#token' is not accessible outside class 'Session' because it has a private identifier), и в отличие от случая с private обойти её нельзя и во время выполнения. Правила времени выполнения описаны в разделе приватные поля в JavaScript.
private или #private: что использовать
private x | #x | |
|---|---|---|
| Кто проверяет | компилятор | движок JavaScript |
Видно в Object.keys / JSON.stringify | да | нет |
Доступ через скобки obj["x"] | разрешён | невозможен |
Подкласс может объявить свой x | нет, будет конфликт | да, у каждого класса свой #x |
Копируется при развёртывании { ...obj } | да | нет |
| Синтаксис для методов | private helper() | #helper() |
Используйте #private, когда данные должны оставаться приватными во время выполнения (токены, внутреннее состояние, которое пользователи библиотеки не должны трогать) или когда нужно, чтобы JSON.stringify их пропускал. Используйте private, когда цель только в чистом публичном API, когда фреймворку нужно читать поле через рефлексию или чтобы соответствовать существующему стилю кодовой базы. Не сочетайте их: private #x это ошибка TS18010 (An accessibility modifier cannot be used with a private identifier).
protected и подклассы
Член protected доступен внутри класса и в любом классе, который его расширяет, но не через экземпляры снаружи.
Одно правило удивляет. Внутри Polygon можно читать sides у this или у другого Polygon, но не у обычного Shape, переданного параметром: other.sides при other: Shape это ошибка TS2446 (Property 'sides' is protected and only accessible through an instance of class 'Polygon'. This is an instance of class 'Shape'.). Подкласс может обращаться к защищённым членам только тех объектов, которые принадлежат его собственной ветви иерархии.
Подкласс может сделать защищённый член публичным, переобъявив его, но не может сделать публичный член защищённым или приватным. Переобъявите его с инициализатором (public override sides = 6) или только как тип (declare public sides: number). Простое public sides: number; отвергается с TS2564 и TS2612, потому что при современных полях классов оно сбросило бы унаследованное значение в undefined после возврата из super().
readonly
readonly запрещает переприсваивание после создания объекта. Поле можно задать в объявлении или в конструкторе; любое более позднее присваивание это ошибка TS2540.
Здесь видны два ограничения. readonly поверхностный: массив нельзя заменить, но его содержимое может меняться (чтобы запретить push, типизируйте его как readonly string[]). И как private, он исчезает во время выполнения, поэтому присваивание, которое отверг компилятор, всё равно выполнилось. Для значения, которое не должно меняться во время выполнения, используйте Object.freeze или геттер без сеттера. Readonly<T> и readonly-массивы описаны на странице о readonly.
readonly сочетается с модификаторами доступа: private readonly cache = new Map<string, number>() это распространённый приём для поля, которое внутреннее и никогда не переприсваивается.
Частые ошибки
- Считать
privateзащитой. Во время выполнения его нет. Используйте#fieldдля данных, которые должны оставаться скрытыми, и никогда не отправляйте объект с секретами прямо вJSON.stringify. - Писать
publicвезде. Это значение по умолчанию; его добавление ничего не меняет. - Использовать
protectedдля всего «внутреннего». Если ни одному подклассу это не нужно,privateоставляет поверхность меньше. - Ожидать, что
readonlyзаморозит вложенные данные. Он только запрещает переприсваивать само свойство.
Часто задаваемые вопросы
Какие модификаторы доступа есть в TypeScript?
public (по умолчанию: доступен везде), protected (внутри класса и его подклассов) и private (только внутри класса). readonly это отдельный модификатор, который запрещает переприсваивание после конструктора и сочетается с любым из трёх.
Чем private отличается от #private в TypeScript?
private проверяет только компилятор; в сгенерированном JavaScript это обычное свойство, поэтому obj["secret"], JSON.stringify и Object.keys его видят. #secret это приватное поле JavaScript: его соблюдает среда выполнения, и код вне класса вообще не может его прочитать.
private в TypeScript действительно приватный?
Только при компиляции. После компиляции поле становится обычным свойством, которое может прочитать любой код на JavaScript. TypeScript даже разрешает доступ к приватным членам через квадратные скобки (obj["field"]) как лазейку. Используйте #field, когда приватность должна сохраняться во время выполнения.
Чем protected отличается от private в TypeScript?
Член private виден только внутри класса, который его объявляет. Член protected виден ещё и внутри подклассов. Ни к одному из них нельзя обратиться через экземпляр снаружи иерархии классов.
Можно ли изменить readonly свойства в TypeScript?
Свойство readonly можно присвоить в его объявлении или в конструкторе и больше нигде (иначе TS2540). Оно поверхностное: readonly tags: string[] нельзя заменить, но tags.push() по-прежнему работает. Чтобы запретить и это, используйте readonly string[].