TS2564: свойство не имеет инициализатора

классы≈ 9 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

В JavaScript поле, которому ничего не присвоили, — это просто undefined, и узнаёте вы об этом в момент падения. В TypeScript строка name: string — обещание: любой, кто получит объект, вправе позвать name.toUpperCase() без единой проверки. TS2564 — это компилятор, который заметил, что обещание нечем подкрепить.

Разделы темы:·

Классы из части I обходились без объявлений: поле появлялось ровно в тот момент, когда конструктор ему что-то присваивал. В TypeScript поля объявляют заранее — и объявление превращается в договор, который компилятор читает буквально.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Две диагностики, обе с одним текстом: «Свойство "name" не имеет инициализатора, и ему не гарантировано присваивание в конструкторе». Формулировка сама называет два выхода — инициализатор при объявлении и присваивание в конструкторе. Запустите пример: предупреждение не отменило выполнение, и TypeError пришёл ровно туда, куда указывал компилятор.

Проверка называется strictPropertyInitialization и включается вместе с strict. Она надстроена над strictNullChecks: без него у типа string нет отдельного значения «пусто», и проверять нечего.

Слово «гарантировано» здесь не для красоты. Компилятор считает присваивание только тогда, когда оно случится при любом сценарии, — на «а тут точно будет больше тысячи» он не ведётся.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Здесь компилятор не придирается, а показывает будущую ошибку: заказ на пятьсот рублей уже создан, а discount по типу число. Дальше по коду кто-нибудь напишет discount.toFixed(2) — и упадёт.

Если у поля есть разумное начальное значение — пустой список, ноль, значение по умолчанию, — пишите его рядом с объявлением. Тип при этом можно не указывать: он выведется, как выводится для const.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

У инициализаторов есть порядок, и он не всегда очевиден: они выполняются сверху вниз, до тела конструктора. Поле, объявленное ниже, для соседа сверху ещё не существует.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Компилятор ловит и это — TS2729, «Свойство "size" используется перед его инициализацией». А выполнение показывает цену ошибки: undefined * 2 даёт NaN, и никакого исключения не будет — число как число, просто неправильное. Поменяйте строки местами и запустите снова.

Когда значение приходит извне, поле заполняет конструктор. Это второй выход, который называет сама диагностика.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Дальше начинается неожиданное. Компилятор ищет присваивание буквально в теле конструктора. Вынесли ту же строчку в метод — и обещание снова считается несдержанным, хотя код работает.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Это не глупость анализатора, а его честность: setup — обычный метод, наследник вправе его переопределить и ничего не присвоить. Гарантии нет, значит и подтвердить её нельзя.

По той же причине компилятор не засчитывает массовое присваивание — он не отслеживает, какие ключи разложит по объекту чужая функция.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Проверка касается только полей экземпляра. Статическое поле без значения — static total: number; — она пропускает молча, и undefined там появится так же легко.

Если логики много, пусть помощник её посчитает и вернёт результат, а присвоит по-прежнему конструктор. Гарантия остаётся видимой, читаемость не страдает.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Первые два способа сдерживают обещание. Но бывает, что поля в момент создания честно ещё нет: пользователь не вошёл, ответ не пришёл, файл не выбран. Тогда чинить надо не инициализацию, а сам тип.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Вопросительный знак снял TS2564 и тут же поставил другой вопрос — TS18048, «Возможно, "session.user" имеет значение undefined». Это и есть выгодный обмен: одна ошибка в объявлении превратилась в напоминание в каждом месте, где значение читают. Дальше работает обычное сужение из главы про объединения и сужение типов.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Четвёртый способ радикальнее: не давать объекту существовать недособранным. Конструктор принимает уже готовые значения, а разбор сырых данных живёт в статическом методе — «полупустого» состояния просто не бывает.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Выбор между третьим и четвёртым способом — это вопрос к предметной области, а не к компилятору: бывает ли отчёт без заголовка? Если нет — собирайте его целиком. Если да — так и напишите в типе.

Есть пятая запись, которую находят первой, потому что её советует любой поиск: token!: string. Восклицательный знак после имени поля означает «я отвечаю, присваивание будет». Компилятор верит и замолкает — не только здесь, но и во всех файлах, где этим классом пользуются.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Ни одной диагностики — и TypeError при выполнении. Это тот самый случай наоборот: обычно вы ждёте ругани компилятора, а получаете тишину и падение. Причём падение не в классе, а у того, кто им пользовался и не имел повода что-то проверять: тип-то говорил string.

Восклицательный знак ничего не делает с кодом — это чистое утверждение для проверки типов, того же рода, что as. Разница с честным способом ровно одна: string | undefined заставляет проверять, а ! запрещает сомневаться.

Соблазн проверить такое поле через in ни к чему не приведёт. Объявление поля — даже с !, даже с ? — превращается в настоящее свойство объекта: на современных настройках компиляции класс создаёт его сразу, со значением undefined.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Остаётся ли ! хоть где-то оправданным? Да — когда поле действительно заполняет код, которого компилятор не видит: внешний каркас, общая подготовка перед каждым тестом. Но это не «способ убрать ошибку», а расписка: проверку сняли вы, значит и последствия ваши.

Отдельная ловушка ждёт тех, кто ставит ! в наследнике — обычно чтобы «уточнить тип» или просто чтобы редактор перестал подчёркивать. Предскажите результат перед запуском.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Кличка пропала. Объявление поля в наследнике — это не пометка для типов, а настоящая инструкция: после super(...) класс заново определяет свойство name, и то, что записал конструктор базы, затирается значением undefined. Компилятор об этом предупреждает — TS2612, «Свойство "name" перезапишет базовое свойство в "Animal"», и прямо называет починку.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

А чаще всего правильный ответ ещё проще: убрать строку совсем. Наследник и так видит все поля базы, повторять объявление нужно только ради declare с уточнённым типом.

ЗаписьЧто видит читающий кодКогда это честно
total = 0всегда числоначальное значение известно заранее
this.id = id в конструкторевсегда строказначение приходит при создании
last?: stringможет не быть — проверьполя правда может ещё не быть
static from() вместо сборки по частямвсегда готовый объектподготовка сложнее одной строки
token!: stringвсегда строка (компилятор поверил на слово)только если заполняет код, которого компилятор не видит

  • TS2564 — не придирка, а невыполненное обещание: тип поля говорит «строка», а в объекте undefined.
  • Два способа обещание сдержать: инициализатор при объявлении и присваивание прямо в теле конструктора.
  • Присваивание в методе или через Object.assign компилятор гарантией не считает — и он прав, гарантии там нет.
  • Два способа сменить обещание: честный тип | undefined с проверкой при чтении или статическая фабрика, собирающая объект целиком.
  • Восклицательный знак не чинит ничего: он снимает проверку, оставляя undefined и перенося падение к тому, кто вашим классом пользуется.
  • Объявление поля в наследнике — настоящая инструкция: оно затирает значение из базы, если это не declare.

1/4В конструкторе вызывается метод this.setup(label), и уже он присваивает this.label. Код работает и печатает нужное. Что скажет компилятор?

2/4Что делает восклицательный знак в объявлении token!: string?

3/4Класс Dog наследует Animal, конструктор базы присваивает this.name. В теле Dog написано name!: string;. Что напечатает new Dog("Рекс").name?

4/4У сессии есть поле с именем вошедшего пользователя, но до входа его честно нет. Какой способ здесь правильный?

Три поля — три разных способа

Компилятор выдаёт три TS2564, и по смыслу поля разные: id известен при создании, visits начинается с нуля, а lastSeen появляется только после первого визита. Почините каждое поле своим способом. Восклицательный знак использовать нельзя.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

id приходит извне — его присваивает конструктор. visits начинается с нуля — это инициализатор. А lastSeen до первого визита честно отсутствует, поэтому меняем не инициализацию, а тип: string | undefined. На склейке строк компилятор смолчит — undefined приклеивается к строке без единой жалобы, — поэтому случай «ещё не заходил» разбираем в report сами.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Обратите внимание на подарок от первого способа: NaN во втором отчёте исчез не потому, что мы его чинили, а потому, что visits теперь всегда число. А третий способ переписал первый отчёт: вместо последний undefined в нём теперь честное последний никогда.

Тишина ценой падения

Компилятор не сказал ни слова, а код падает на первой же строке вывода. Объясните, почему диагностики нет, и почините так, чтобы поиск по пустому каталогу возвращал -1.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Диагностики нет, потому что восклицательный знак — это обещание автора, и компилятор его принял: тип поля Map, сомневаться незачем. Но build никто не вызвал, поле осталось пустым, и undefined.get дал TypeError. Здесь у поля есть очевидное начальное значение — пустая карта, поэтому чинится это инициализатором.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Пустой каталог теперь не особое состояние, а обычное: искать в нём можно, находится ничего.

Куда делся текст

Класс Pinned ничего не меняет в поведении базы, но preview() падает. Найдите причину — компилятор её называет — и уберите её.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Строка text!: string; в наследнике — не уточнение типа, а объявление поля: после super(...) свойство определяется заново и получает undefined. Компилятор говорит об этом прямо — TS2612, «Свойство "text" перезапишет базовое свойство в "Note"». Поле базы наследник и так видит, поэтому лишнюю строку достаточно удалить.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Если объявление всё же нужно — например, чтобы сузить тип поля в наследнике, — ставьте declare: оно живёт только в типах и в JavaScript не попадает.