Класс 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 и геттеры.