Menu

Классы в TypeScript: поля, методы, геттеры и implements

Классы TypeScript это классы JavaScript с типизированными полями, методами и конструкторами. Как работают объявления полей и strictPropertyInitialization, как типизировать this, геттеры и сеттеры, статические члены, implements и почему класс одновременно является типом.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Класс TypeScript это класс JavaScript с типами: каждое поле объявляется с типом в теле класса, а методы и конструктор получают типизированные параметры и возвращаемые значения.

Скомпилированный результат это обычный класс JavaScript без типов, поэтому всё, что сказано о классах JavaScript, по-прежнему в силе. То, что добавляет TypeScript, происходит при компиляции: последний вызов отвергается ещё до запуска программы.

Объявление полей

Полю нужно объявление в теле класса, прежде чем конструктор сможет его присвоить. Присваивание this.owner без объявления owner это ошибка TS2339 (свойство не существует). Поле с инициализатором получает тип, выведенный из значения, как переменная.

ОбъявлениеЗначение
name: string;Должно быть присвоено в конструкторе
count = 0;Инициализировано, тип выведен как number
label?: string;Необязательное, тип string | undefined
readonly id: number;Присваивается один раз, затем только чтение
data!: string[];Явное присваивание: поверьте, значение будет задано
static total = 0;Принадлежит классу, а не экземплярам

При включённом strict параметр strictPropertyInitialization проверяет, что каждое необязательное поле получает значение. Этот класс забывает два:

Компилятор сообщает об обоих:

index.ts(3,5): error TS2564: Property 'name' has no initializer and is not definitely assigned in the constructor.
index.ts(4,5): error TS2564: Property 'age' has no initializer and is not definitely assigned in the constructor.

Проверка существует потому, что без неё new Profile().name.toUpperCase() прошло бы проверку типов, а затем упало бы на undefined. Каждое поле можно исправить одним из четырёх способов: задать инициализатор, присвоить значение в конструкторе, сделать поле необязательным через ? или написать name!: string, если поле позже задаёт фреймворк или метод init(). Последний способ отключает проверку для этого поля, поэтому пользуйтесь им редко.

Методы и this

Методы типизируются как функции. Внутри метода this имеет тип экземпляра. Метод может вернуть this, чтобы можно было строить цепочки вызовов, и тип возврата автоматически следует за подклассами.

class QueryBuilder {
    private parts: string[] = [];

    where(condition: string): this {
        this.parts.push(condition);
        return this;
    }

    build(): string {
        return this.parts.join(" AND ");
    }
}

const sql = new QueryBuilder().where("age > 18").where("active = 1").build();
console.log(sql); // age > 18 AND active = 1

Метод теряет свой this, если его отделить от объекта; это классическая ошибка JavaScript. По умолчанию TypeScript её не ловит, но ловит, если метод объявляет параметр this. Этот параметр стирается из вывода и только охраняет место вызова:

Поле со стрелочной функцией работает, потому что каждый экземпляр получает собственную функцию, замыкающую this. Цена этого: по одной функции на экземпляр вместо одного общего метода в прототипе.

Геттеры и сеттеры

Аксессоры get и set снаружи выглядят как свойства. Геттер без сеттера доступен только для чтения: присваивание ему это ошибка компиляции (TS2540).

Компилятор отвергает присваивание, а если проверку обойти, во время выполнения тоже будет TypeError, потому что скомпилированный файл работает в строгом режиме (компилятор выводит "use strict" в начале). RangeError в сеттере это проверка времени выполнения, которую вы написали сами. Тип number говорит лишь, что значение является числом; о диапазонах он ничего не знает.

Статические члены

Поля и методы static принадлежат самому классу. К ним обращаются через имя класса, а не через экземпляр. Блок static { } выполняется один раз при определении класса.

Статические члены не могут использовать параметры типа класса: в class Box<T> поле static empty: T это ошибка TS2302, потому что статическое поле одно и общее для всех Box<string>, Box<number> и так далее.

Реализация интерфейса

implements просит компилятор проверить, что в классе есть всё, чего требует интерфейс. Отсутствующий член это ошибка TS2420: Class 'X' incorrectly implements interface 'Y'.

Класс может реализовать несколько интерфейсов: class Doc implements Printable, Serializable. Двух вещей implements не делает. Он не типизирует за вас параметры методов класса: check(s) { ... } внутри класса, реализующего check(s: string): boolean, всё равно даёт ошибку TS7006 (Parameter 's' implicitly has an 'any' type), так что аннотируйте их. И он не оставляет следа во время выполнения, поэтому obj instanceof Shape невозможно.

private radius в конструкторе это свойство-параметр: оно объявляет и присваивает поле за один шаг. См. конструкторы.

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

Объявление класса создаёт две вещи с одним именем: значение (функцию-конструктор, которую вызывают через new) и тип (структуру экземпляра). Поскольку TypeScript сравнивает типы по структуре, под этот тип подходит любой объект с теми же публичными членами, даже созданный не этим классом.

Обычный объект проходит проверку типов, но во время выполнения не является Point: ({ x: 6, y: 8 }) instanceof Point равно false. Класс с членом private или #private это пресекает: присвоить ему можно только экземпляры этого класса (или его подклассов).

Частые ошибки

  • Объявить поле и никогда его не присвоить. TS2564 сообщает, что поле было бы undefined. Инициализируйте его, а не заглушайте через !.
  • Передать метод как колбэк. button.onclick = obj.handle теряет this. Используйте поле со стрелочной функцией или obj.handle.bind(obj).
  • Ожидать, что implements добавит код или типы. Он только проверяет. Типам параметров по-прежнему нужны аннотации.
  • Использовать instanceof с интерфейсом. Интерфейсов во время выполнения нет; проверяйте класс или используйте защитник типа.
  • Считать, что типы валидируют данные. Поле number при компиляции принимает любое число, а во время выполнения из нетипизированных данных вообще что угодно.

Часто задаваемые вопросы

Как создать класс в TypeScript?

Объявите каждое поле с типом в теле класса, затем напишите конструктор и методы как в JavaScript: class User { name: string; constructor(name: string) { this.name = name; } greet(): string { return "Hi, " + this.name; } }. Экземпляр создаётся через new User("Ada").

Что означает "has no initializer and is not definitely assigned in the constructor"?

Это ошибка TS2564 из strictPropertyInitialization (входит в strict). Поле с типом string начиналось бы с undefined, потому что его ничто не задаёт. Исправьте это инициализатором (name = ""), присваиванием в конструкторе, необязательным полем (name?: string) или, если поле задаётся вне конструктора, утверждением явного присваивания (name!: string).

Чем implements отличается от extends в TypeScript?

extends наследует код родительского класса: его поля и методы существуют у потомка во время выполнения. implements только просит компилятор проверить, что класс имеет структуру интерфейса. Он ничего не добавляет в класс и исчезает из скомпилированного JavaScript.

Можно ли использовать класс как тип в TypeScript?

Да. Имя класса это одновременно значение (конструктор) и тип (структура его экземпляров). let u: User принимает любой объект с теми же публичными членами, потому что TypeScript сравнивает типы структурно. typeof User это тип самого конструктора.

Отличаются ли классы TypeScript от классов JavaScript во время выполнения?

Нет. TypeScript компилирует класс в обычный класс JavaScript. Аннотации типов, конструкции implements и модификаторы вроде private стираются; во время выполнения существуют только возможности JavaScript, такие как поля #private, блоки static и геттеры.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ