Перейти к содержимому
Теория · Классы · урок 1 из 12

Поля и конструктор

Чему вы научитесь

Чему вы научитесь:

  • объявлять поля класса с типами и находить поле, которое осталось без значения;
  • закрывать поля через private, protected и # и знать, что из этого работает при запуске;
  • защищать поля через readonly и сокращать конструктор параметрами-свойствами;
  • переопределять методы через override и задавать общие правила через abstract и implements.
Чертёж class, приоткрытая дверь с бумажной табличкой private и запертая на засов дверь

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

В JavaScript чертёж бывает неполным. Поле появляется в объекте тогда, когда конструктор или метод записал что-то в this. Ошибётесь в имени поля внутри метода, и JavaScript молча заведёт новое поле с опечаткой в имени.

В TypeScript все поля перечисляют в теле класса, и у каждого есть тип. Компилятор работает как инспектор на стройке: сверяет с чертежом каждое обращение к полю. Вот счётчик кликов:

TypeScript
class Counter {
    count: number;
    step = 1;

    constructor(start: number) {
        this.count = start;
    }

    next(): number {
        this.count += this.step;
        return this.count;
    }
}

console.log(new Counter(10).next()); // 11

Давайте разберём класс по строкам:

  1. count: number; объявляет поле count с типом number. Значения здесь нет, его даст конструктор.
  2. step = 1; объявляет поле сразу со значением. Тип TypeScript выводит из значения: number.
  3. Конструктор принимает start: number и кладёт его в this.count.
  4. Метод next(): number прибавляет шаг и возвращает новое значение. Тип результата проверяется так же, как у обычной функции.
  5. new Counter(10).next() печатает 11.

Поле с типом

Объявление поля похоже на объявление переменной, только без let:

  • имя поля — count;
  • двоеточие и тип — : number, такая же аннотация, как у переменной;
  • начальное значение после = — необязательная часть, как = 1 у поля step.

Если значение есть, тип можно не писать: TypeScript выведет его сам. Поэтому у step нет аннотации, а поле label = "clicks" получило бы тип string.

Внутри методов tsc знает тип каждого поля. Запись this.count = "много" он не пропустит, как не пропустил бы такое присваивание переменной типа number.

Каждое поле получает значение

Строгий режим проверяет ещё одну вещь: к концу конструктора у каждого поля должно быть значение. Уберём из счётчика конструктор:

TypeScript
class Counter {
    count: number; // ✗
    step = 1;
}

console.log(new Counter().step);
TEXT
error TS2564: Property 'count' has no initializer and is not definitely assigned in the constructor.

Компилятор отвечает: «у свойства count нет начального значения, и конструктор не гарантирует, что присвоит его». В JavaScript такое поле было бы undefined, а тип обещает число. Ошибка ловит это расхождение до запуска.

Исправить можно тремя способами:

  • дать полю значение в объявлении: count = 0;;
  • присвоить ему значение в конструкторе, как в первом примере;
  • разрешить полю быть пустым: у поля count?: number; тип number | undefined, и перед использованием его нужно проверять.

Слово «гарантирует» здесь важно. Присваивание внутри if не считается: если условие не выполнится, поле останется пустым. Не считается и присваивание в методе, который вызывает конструктор: tsc смотрит только на сам конструктор.

Класс — это ещё и тип

Имя класса можно писать везде, где пишут тип: у переменной, у параметра, у результата функции.

TypeScript
class Counter {
    count = 0;
    step = 1;
}

function show(c: Counter): string {
    return `${c.count} (+${c.step})`;
}

console.log(show(new Counter())); // 0 (+1)
console.log(show({ count: 5, step: 2 })); // 5 (+2)
  1. show принимает параметр типа Counter и читает его поля.
  2. Первый вызов передаёт объект, созданный через new Counter(), и печатает 0 (+1).
  3. Второй вызов передаёт обычный объект без всякого new, и tsc его тоже пропускает. Программа печатает 5 (+2).

Здесь аналогия с чертежом перестаёт работать. Настоящий инспектор спросит, по какому чертежу строили дом. TypeScript сравнивает только форму: подходит любой объект с полями count и step нужных типов. Это та же структурная типизация, что и у интерфейсов.

Частая ошибка

Поле не объявили, а сразу записали в конструкторе, как привыкли в JavaScript:

TypeScript
class Counter {
    constructor(start: number) {
        this.count = start; // ✗
    }
}
TEXT
error TS2339: Property 'count' does not exist on type 'Counter'.

В JavaScript запись в this создаёт поле на ходу. TypeScript сверяется с чертежом: в теле класса поля count нет, значит, и записать в него нельзя. Добавьте объявление count: number; над конструктором. Та же проверка ловит опечатки: на this.cuont компилятор скажет, что такого свойства нет, и предложит count.

Главное

  • Поля класса объявляют в его теле с типом, например count: number;, а у поля со значением тип выводится сам.
  • В строгом режиме каждое поле получает значение в объявлении или в конструкторе, иначе будет ошибка TS2564.
  • Записать в this поле, которого нет среди объявленных, нельзя: ошибка TS2339.
  • Имя класса — это и тип, и к нему подходит любой объект той же формы.