Класс
Общий синтаксис класса
Ключевое слово class используется в TypeScript для определения класса. Ниже приведён пример:
class Person { private name: string; private age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } public sayHi(): void { console.log( `Hello, my name is ${this.name} and I am ${this.age} years old.` ); }}Ключевое слово class используется для определения класса с именем “Person”.
Класс имеет два закрытых свойства: name с типом string и age с типом number.
Конструктор определяется с помощью ключевого слова constructor. Он принимает name и age в качестве параметров и присваивает их соответствующим свойствам.
Класс имеет public-метод sayHi, который выводит приветственное сообщение в консоль.
Чтобы создать экземпляр класса в TypeScript, можно использовать ключевое слово new, после которого следуют имя класса и круглые скобки (). Например:
const myObject = new Person('John Doe', 25);myObject.sayHi(); // Output: Hello, my name is John Doe and I am 25 years old.Конструктор
Конструкторы — это специальные методы класса, которые используются для инициализации свойств объекта при создании экземпляра класса.
class Person { public name: string; public age: number;
constructor(name: string, age: number) { this.name = name; this.age = age; }
sayHello() { console.log( `Hello, my name is ${this.name} and I'm ${this.age} years old.` ); }}
const john = new Person('Simon', 17);john.sayHello();Конструктор можно перегрузить, используя следующий синтаксис:
type Sex = 'm' | 'f';
class Person { name: string; age: number; sex: Sex;
constructor(name: string, age: number, sex?: Sex); constructor(name: string, age: number, sex: Sex) { this.name = name; this.age = age; this.sex = sex ?? 'm'; }}
const p1 = new Person('Simon', 17);const p2 = new Person('Alice', 22, 'f');В TypeScript можно определить несколько перегрузок конструктора, однако реализация может быть только одна и должна быть совместима со всеми перегрузками. Этого можно добиться с помощью необязательного параметра.
class Person { name: string; age: number;
constructor(); constructor(name: string); constructor(name: string, age: number); constructor(name?: string, age?: number) { this.name = name ?? 'Unknown'; this.age = age ?? 0; }
displayInfo() { console.log(`Name: ${this.name}, Age: ${this.age}`); }}
const person1 = new Person();person1.displayInfo(); // Name: Unknown, Age: 0
const person2 = new Person('John');person2.displayInfo(); // Name: John, Age: 0
const person3 = new Person('Jane', 25);person3.displayInfo(); // Name: Jane, Age: 25Закрытые и защищённые конструкторы
В TypeScript конструкторы можно объявить закрытыми или защищёнными, что ограничивает их доступность и использование.
Закрытые конструкторы: Могут вызываться только внутри самого класса. Закрытые конструкторы часто используются, когда необходимо реализовать паттерн «Одиночка» или разрешить создание экземпляров только фабричному методу внутри класса.
Защищённые конструкторы: Защищённые конструкторы полезны, когда необходимо создать базовый класс, экземпляры которого не следует создавать напрямую, но от которого могут наследоваться подклассы.
class BaseClass { protected constructor() {}}
class DerivedClass extends BaseClass { private value: number;
constructor(value: number) { super(); this.value = value; }}
// Attempting to instantiate the base class directly will result in an error// const baseObj = new BaseClass(); // Error: Constructor of class 'BaseClass' is protected.
// Create an instance of the derived classconst derivedObj = new DerivedClass(10);Модификаторы доступа
Модификаторы доступа private, protected и public используются для управления видимостью и доступностью членов классов TypeScript, таких как свойства и методы. Эти модификаторы необходимы для обеспечения инкапсуляции и установления границ доступа к внутреннему состоянию класса и его изменению.
Модификатор private разрешает доступ к члену класса только внутри содержащего его класса.
Модификатор protected разрешает доступ к члену класса внутри содержащего его класса и производных классов.
Модификатор public предоставляет неограниченный доступ к члену класса, позволяя обращаться к нему откуда угодно.
Геттеры и сеттеры
Геттеры и сеттеры — это специальные методы, позволяющие определять пользовательское поведение при доступе к свойствам класса и их изменении. Они позволяют инкапсулировать внутреннее состояние объекта и добавлять логику при получении или установке значений свойств.
В TypeScript геттеры и сеттеры определяются с помощью ключевых слов get и set соответственно. Рассмотрим пример:
class MyClass { private _myProperty: string;
constructor(value: string) { this._myProperty = value; } get myProperty(): string { return this._myProperty; } set myProperty(value: string) { this._myProperty = value; }}Автоаксессоры в классах
В TypeScript версии 4.9 добавлена поддержка автоаксессоров — планируемой возможности ECMAScript. Они похожи на свойства класса, но объявляются с помощью ключевого слова “accessor”.
class Animal { accessor name: string;
constructor(name: string) { this.name = name; }}Автоаксессоры преобразуются в закрытые аксессоры get и set, работающие с недоступным извне свойством.
class Animal { #__name: string;
get name() { return this.#__name; } set name(value: string) { this.#__name = value; }
constructor(name: string) { this.name = name; }}this
В TypeScript ключевое слово this внутри методов или конструкторов класса ссылается на текущий экземпляр класса. Оно позволяет получать доступ к свойствам и методам класса и изменять их из области видимости самого класса.
Это даёт возможность получать доступ к внутреннему состоянию объекта и управлять им в его собственных методах.
class Person { private name: string; constructor(name: string) { this.name = name; } public introduce(): void { console.log(`Hello, my name is ${this.name}.`); }}
const person1 = new Person('Alice');person1.introduce(); // Hello, my name is Alice.Свойства-параметры
Свойства-параметры позволяют объявлять и инициализировать свойства класса непосредственно в параметрах конструктора, избегая повторяющегося шаблонного кода. Например:
class Person { constructor( private name: string, public age: number ) { // The "private" and "public" keywords in the constructor // automatically declare and initialize the corresponding class properties. } public introduce(): void { console.log( `Hello, my name is ${this.name} and I am ${this.age} years old.` ); }}const person = new Person('Alice', 25);person.introduce();Абстрактные классы
Абстрактные классы используются в TypeScript главным образом для наследования. Они позволяют определять общие свойства и методы, которые могут наследоваться подклассами. Это полезно, когда требуется определить общее поведение и обязать подклассы реализовать определённые методы. Абстрактные классы позволяют создать иерархию классов, в которой абстрактный базовый класс предоставляет подклассам общий интерфейс и общую функциональность.
abstract class Animal { protected name: string;
constructor(name: string) { this.name = name; }
abstract makeSound(): void;}
class Cat extends Animal { makeSound(): void { console.log(`${this.name} meows.`); }}
const cat = new Cat('Whiskers');cat.makeSound(); // Output: Whiskers meows.С обобщёнными типами
Классы с обобщёнными типами позволяют определять классы, пригодные для повторного использования и способные работать с разными типами.
class Container<T> { private item: T;
constructor(item: T) { this.item = item; }
getItem(): T { return this.item; }
setItem(item: T): void { this.item = item; }}
const container1 = new Container<number>(42);console.log(container1.getItem()); // 42
const container2 = new Container<string>('Hello');container2.setItem('World');console.log(container2.getItem()); // WorldДекораторы
Декораторы предоставляют механизм для добавления метаданных, изменения поведения, проверки или расширения функциональности целевого элемента. Это функции, выполняемые во время работы программы. К одному объявлению можно применить несколько декораторов.
Декораторы являются экспериментальной возможностью, а следующие примеры совместимы только с TypeScript версии 5 или выше при использовании ES6.
В версиях TypeScript до 5 их необходимо включить с помощью свойства experimentalDecorators в файле tsconfig.json или параметра --experimentalDecorators в командной строке (но следующий пример работать не будет).
Некоторые распространённые сценарии использования декораторов:
- Отслеживание изменений свойств.
- Отслеживание вызовов методов.
- Добавление дополнительных свойств или методов.
- Проверка во время выполнения.
- Автоматическая сериализация и десериализация.
- Логирование.
- Авторизация и аутентификация.
- Защита от ошибок.
Примечание: декораторы в версии 5 не позволяют декорировать параметры.
Типы декораторов:
Декораторы классов
Декораторы классов полезны для расширения существующего класса, например для добавления свойств или методов либо сбора экземпляров класса. В следующем примере мы добавляем метод toString, преобразующий класс в строковое представление.
type Constructor<T = {}> = new (...args: any[]) => T;
function toString<Class extends Constructor>( Value: Class, context: ClassDecoratorContext<Class>) { return class extends Value { constructor(...args: any[]) { super(...args); console.log(JSON.stringify(this)); console.log(JSON.stringify(context)); } };}
@toStringclass Person { name: string;
constructor(name: string) { this.name = name; }
greet() { return 'Hello, ' + this.name; }}const person = new Person('Simon');/* Logs:{"name":"Simon"}{"kind":"class","name":"Person"}*/Декоратор свойства
Декораторы свойств полезны для изменения поведения свойства, например для изменения начальных значений. В следующем коде приведён скрипт, который всегда задаёт значение свойства в верхнем регистре:
function upperCase<T>( target: undefined, context: ClassFieldDecoratorContext<T, string>) { return function (this: T, value: string) { return value.toUpperCase(); };}
class MyClass { @upperCase prop1 = 'hello!';}
console.log(new MyClass().prop1); // Logs: HELLO!Декоратор метода
Декораторы методов позволяют изменять или расширять поведение методов. Ниже приведён пример простого логгера:
function log<This, Args extends any[], Return>( target: (this: This, ...args: Args) => Return, context: ClassMethodDecoratorContext< This, (this: This, ...args: Args) => Return >) { const methodName = String(context.name);
function replacementMethod(this: This, ...args: Args): Return { console.log(`LOG: Entering method '${methodName}'.`); const result = target.call(this, ...args); console.log(`LOG: Exiting method '${methodName}'.`); return result; }
return replacementMethod;}
class MyClass { @log sayHello() { console.log('Hello!'); }}
new MyClass().sayHello();Будет выведено:
LOG: Entering method 'sayHello'.Hello!LOG: Exiting method 'sayHello'.Декораторы геттеров и сеттеров
Декораторы геттеров и сеттеров позволяют изменять или расширять поведение аксессоров класса. Они полезны, например, для проверки значений, присваиваемых свойствам. Ниже приведён простой пример декоратора геттера:
function range<This, Return extends number>(min: number, max: number) { return function ( target: (this: This) => Return, context: ClassGetterDecoratorContext<This, Return> ) { return function (this: This): Return { const value = target.call(this); if (value < min || value > max) { throw 'Invalid'; } Object.defineProperty(this, context.name, { value, enumerable: true, }); return value; }; };}
class MyClass { private _value = 0;
constructor(value: number) { this._value = value; } @range(1, 100) get getValue(): number { return this._value; }}
const obj = new MyClass(10);console.log(obj.getValue); // Valid: 10
const obj2 = new MyClass(999);console.log(obj2.getValue); // Throw: Invalid!Метаданные декораторов
Метаданные декораторов упрощают применение и использование метаданных в любом классе. Декораторы могут обращаться к новому свойству metadata объекта контекста, которое может служить ключом как для примитивов, так и для объектов.
Доступ к метаданным класса можно получить через Symbol.metadata.
Метаданные можно использовать для различных целей, таких как отладка, сериализация или внедрение зависимостей с помощью декораторов.
//@ts-ignoreSymbol.metadata ??= Symbol('Symbol.metadata'); // Simple polyfill
type Context = | ClassFieldDecoratorContext | ClassAccessorDecoratorContext | ClassMethodDecoratorContext; // Context contains property metadata: DecoratorMetadata
function setMetadata(_target: any, context: Context) { // Set the metadata object with a primitive value context.metadata[context.name] = true;}
class MyClass { @setMetadata a = 123;
@setMetadata accessor b = 'b';
@setMetadata fn() {}}
const metadata = MyClass[Symbol.metadata]; // Get metadata information
console.log(JSON.stringify(metadata)); // {"bar":true,"baz":true,"foo":true}Наследование
Наследование — это механизм, благодаря которому класс может наследовать свойства и методы другого класса, называемого базовым классом или суперклассом. Производный класс, также называемый дочерним классом или подклассом, может расширять и специализировать функциональность базового класса, добавляя новые свойства и методы либо переопределяя существующие.
class Animal { name: string;
constructor(name: string) { this.name = name; }
speak(): void { console.log('The animal makes a sound'); }}
class Dog extends Animal { breed: string;
constructor(name: string, breed: string) { super(name); this.breed = breed; }
speak(): void { console.log('Woof! Woof!'); }}
// Create an instance of the base classconst animal = new Animal('Generic Animal');animal.speak(); // The animal makes a sound
// Create an instance of the derived classconst dog = new Dog('Max', 'Labrador');dog.speak(); // Woof! Woof!"TypeScript не поддерживает множественное наследование в традиционном смысле и вместо этого допускает наследование от одного базового класса. TypeScript поддерживает использование нескольких интерфейсов. Интерфейс может определять контракт структуры объекта, а класс может реализовывать несколько интерфейсов. Благодаря этому класс может наследовать поведение и структуру из нескольких источников.
interface Flyable { fly(): void;}
interface Swimmable { swim(): void;}
class FlyingFish implements Flyable, Swimmable { fly() { console.log('Flying...'); }
swim() { console.log('Swimming...'); }}
const flyingFish = new FlyingFish();flyingFish.fly();flyingFish.swim();Ключевое слово class в TypeScript, как и в JavaScript, часто называют синтаксическим сахаром. Оно было введено в ECMAScript 2015 (ES6), чтобы предоставить более привычный синтаксис для создания объектов и работы с ними в стиле классов. Однако важно отметить, что TypeScript, являясь надмножеством JavaScript, в конечном счёте компилируется в JavaScript, который по своей сути остаётся прототипно-ориентированным.
Статические члены
TypeScript поддерживает статические члены. Для доступа к статическим членам класса можно указать имя класса и поставить после него точку, не создавая объект.
class OfficeWorker { static memberCount: number = 0;
constructor(private name: string) { OfficeWorker.memberCount++; }}
const w1 = new OfficeWorker('James');const w2 = new OfficeWorker('Simon');const total = OfficeWorker.memberCount;console.log(total); // 2Инициализация свойств
В TypeScript существует несколько способов инициализировать свойства класса:
Непосредственно в объявлении:
В следующем примере эти начальные значения будут использованы при создании экземпляра класса.
class MyClass { property1: string = 'default value'; property2: number = 42;}В конструкторе:
class MyClass { property1: string; property2: number;
constructor() { this.property1 = 'default value'; this.property2 = 42; }}С помощью параметров конструктора:
class MyClass { constructor( private property1: string = 'default value', public property2: number = 42 ) { // There is no need to assign the values to the properties explicitly. } log() { console.log(this.property2); }}const x = new MyClass();x.log();Перегрузка методов
Перегрузка методов позволяет классу иметь несколько методов с одинаковым именем, но с разными типами или количеством параметров. Это позволяет вызывать метод разными способами в зависимости от переданных аргументов.
class MyClass { add(a: number, b: number): number; // Overload signature 1 add(a: string, b: string): string; // Overload signature 2
add(a: number | string, b: number | string): number | string { if (typeof a === 'number' && typeof b === 'number') { return a + b; } if (typeof a === 'string' && typeof b === 'string') { return a.concat(b); } throw new Error('Invalid arguments'); }}
const r = new MyClass();console.log(r.add(10, 5)); // Logs 15