Spec-Zone.ru › TypeScript 5.1

Классы

Дополнительное чтение:
Классы (MDN)

TypeScript полностью поддерживает ключевое слово class , введённое в ES2015.

Как и другие языковые возможности JavaScript, TypeScript добавляет аннотации типов и другой синтаксис, чтобы позволить вам выражать взаимосвязи между классами и другими типами.

Члены класса

Вот самый базовый класс — пустой:

class Point {}

Этот класс пока не очень полезен, поэтому давайте начнём добавлять члены.

Поля

Объявление поля создаёт общедоступное записываемое свойство в классе:

class Point {
  x: number;
  y: number;
}
 
const pt = new Point();
pt.x = 0;
pt.y = 0;

Как и в других местах, аннотация типа необязательна, но будет неявной any , если не указана.

Поля также могут иметь инициализаторы; они будут выполняться автоматически при создании экземпляра класса:

class Point {
  x = 0;
  y = 0;
}
 
const pt = new Point();
// Prints 0, 0
console.log(`${pt.x}, ${pt.y}`);

Как и в случае с const, let, и var, инициализатор свойства класса будет использоваться для вывода его типа:

const pt = new Point();
pt.x = "0";

--strictPropertyInitialization

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

class BadGreeter {
  name: string;
}
class GoodGreeter {
  name: string;
 
  constructor() {
    this.name = "hello";
  }
}

Обратите внимание, что поле должно быть инициализировано в самом конструкторе. TypeScript не анализирует методы, которые вы вызываете из конструктора, чтобы обнаружить инициализации, поскольку производный класс может переопределить эти методы и не инициализировать члены.

Если вы намерены определённо инициализировать поле другими способами, кроме конструктора (например, возможно, внешняя библиотека заполняет часть вашего класса за вас), вы можете использовать оператор утверждения определённой инициализации, !:

class OKGreeter {
  // Not initialized, but no error
  name!: string;
}

readonly

Поля могут быть снабжены модификатором readonly . Это предотвращает присвоение значений полю вне конструктора.

class Greeter {
  readonly name: string = "world";
 
  constructor(otherName?: string) {
    if (otherName !== undefined) {
      this.name = otherName;
    }
  }
 
  err() {
    this.name = "not ok";
  }
}
const g = new Greeter();
g.name = "also not ok";

Конструкторы

Дополнительное чтение:
Конструктор (MDN)

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

class Point {
  x: number;
  y: number;
 
  // Normal signature with defaults
  constructor(x = 0, y = 0) {
    this.x = x;
    this.y = y;
  }
}
class Point {
  // Overloads
  constructor(x: number, y: string);
  constructor(s: string);
  constructor(xs: any, y?: any) {
    // TBD
  }
}

Существует всего несколько отличий между сигнатурами конструктора класса и сигнатурами функций:

  • Конструкторы не могут иметь параметров типа — они относятся к внешнему объявлению класса, о котором мы узнаем позже
  • Конструкторы не могут иметь аннотаций типа возвращаемого значения — типом возвращаемого значения всегда является экземпляр класса

Вызовы super

Как и в JavaScript, если у вас есть базовый класс, вам необходимо вызвать super(); в теле конструктора перед использованием любых this. членов:

class Base {
  k = 4;
}
 
class Derived extends Base {
  constructor() {
    // Prints a wrong value in ES5; throws exception in ES6
    console.log(this.k);
    super();
  }
}

Забыть вызвать super — распространённая ошибка в JavaScript, но TypeScript сообщит вам об этом, когда это необходимо.

Методы

Дополнительное чтение:
Определения методов

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

class Point {
  x = 10;
  y = 10;
 
  scale(n: number): void {
    this.x *= n;
    this.y *= n;
  }
}

Помимо стандартных аннотаций типов, TypeScript не добавляет ничего нового в методы.

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

let x: number = 0;
 
class C {
  x: string = "hello";
 
  m() {
    // This is trying to modify 'x' from line 1, not the class property
    x = "world";
  }
}

Геттеры/Сеттеры

Классы также могут иметь акцессоры:

class C {
  _length = 0;
  get length() {
    return this._length;
  }
  set length(value) {
    this._length = value;
  }
}

Обратите внимание, что пара геттер/сеттер, основанная на поле и без дополнительной логики, очень редко полезна в JavaScript. Вполне приемлемо экспонировать общедоступные поля, если вам не нужно добавлять дополнительную логику во время операций геттер/сеттер.

TypeScript имеет некоторые специальные правила вывода для акцессоров:

  • Если get существует, но нет set, свойство автоматически readonly
  • Если тип параметра сеттера не указан, он выводится из типа возвращаемого значения геттера
  • Геттеры и сеттеры должны иметь ту же видимость члена

С TypeScript 4.3 стало возможным иметь акцессоры с разными типами для получения и задания значений.

class Thing {
  _size = 0;
 
  get size(): number {
    return this._size;
  }
 
  set size(value: string | number | boolean) {
    let num = Number(value);
 
    // Don't allow NaN, Infinity, etc
 
    if (!Number.isFinite(num)) {
      this._size = 0;
      return;
    }
 
    this._size = num;
  }
}

Подписи индексов

Классы могут объявлять подписи индексов; они работают так же, как Подписи индексов для других типов объектов:

class MyClass {
  [s: string]: boolean | ((s: string) => boolean);
 
  check(s: string) {
    return this[s] as boolean;
  }
}

Поскольку тип подписи индекса также должен охватывать типы методов, использовать эти типы не так просто. В целом лучше хранить индексированные данные в другом месте, а не в самом экземпляре класса.

Наследование классов

Как и другие языки с объектно-ориентированными возможностями, классы в JavaScript могут наследоваться от базовых классов.

implements Определения

Вы можете использовать определение implements , чтобы проверить, удовлетворяет ли класс определённому interface. Если класс не реализует его правильно, будет выдано сообщение об ошибке:

interface Pingable {
  ping(): void;
}
 
class Sonar implements Pingable {
  ping() {
    console.log("ping!");
  }
}
 
class Ball implements Pingable {
  pong() {
    console.log("pong!");
  }
}

Классы также могут реализовывать несколько интерфейсов, например class C implements A, B {.

Осторожность

Важно понимать, что определение implements — это только проверка, что класс может рассматриваться как тип интерфейса. Это не меняет тип класса или его методов вовсе. Распространённая ошибка заключается в предположении, что определение implements изменит тип класса — этого не происходит!

interface Checkable {
  check(name: string): boolean;
}
 
class NameChecker implements Checkable {
  check(s) {
    // Notice no error here
    return s.toLowerCase() === "ok";
  }
}

В этом примере мы, возможно, ожидали, что тип s будет под влиянием параметра name: string из check. Этого не происходит — определения implements не изменяют способ проверки тела класса или вывод его типа.

Аналогично, реализация интерфейса с необязательным свойством не создаёт это свойство:

interface A {
  x: number;
  y?: number;
}
class C implements A {
  x = 0;
}
const c = new C();
c.y = 10;

extends Определения

Дополнительное чтение:
Ключевое слово extends (MDN)

Классы могут extend от базового класса. Производный класс имеет все свойства и методы базового класса и также может определять дополнительные члены.

class Animal {
  move() {
    console.log("Moving along!");
  }
}
 
class Dog extends Animal {
  woof(times: number) {
    for (let i = 0; i < times; i++) {
      console.log("woof!");
    }
  }
}
 
const d = new Dog();
// Base class method
d.move();
// Derived class method
d.woof(3);

Переопределение методов

Дополнительное чтение:
Ключевое слово super (MDN)

Производный класс также может переопределить поле или свойство базового класса. Вы можете использовать синтаксис super. , чтобы получить доступ к методам базового класса. Обратите внимание, что поскольку классы JavaScript — это просто объекты поиска, не существует понятия «сверхполя».

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

Например, вот допустимый способ переопределения метода:

class Base {
  greet() {
    console.log("Hello, world!");
  }
}
 
class Derived extends Base {
  greet(name?: string) {
    if (name === undefined) {
      super.greet();
    } else {
      console.log(`Hello, ${name.toUpperCase()}`);
    }
  }
}
 
const d = new Derived();
d.greet();
d.greet("reader");

Важно, чтобы производный класс соблюдал контракт базового класса. Помните, что очень часто (и всегда допустимо!) обращаться к экземпляру производного класса через ссылку на базовый класс:

// Alias the derived instance through a base class reference
const b: Base = d;
// No problem
b.greet();

Что произойдёт, если Derived не соблюдает контракт Base?

class Base {
  greet() {
    console.log("Hello, world!");
  }
}
 
class Derived extends Base {
  // Make this parameter required
  greet(name: string) {
    console.log(`Hello, ${name.toUpperCase()}`);
  }
}

Если мы скомпилируем этот код несмотря на ошибку, этот пример аварийно завершит работу:

const b: Base = new Derived();
// Crashes because "name" will be undefined
b.greet();

Объявления полей только для типа

Когда target >= ES2022 или useDefineForClassFields true, поля класса инициализируются после завершения конструктора родительского класса, перезаписывая любое значение, заданное родительским классом. Это может быть проблемой, когда вы хотите только переопределить более точный тип для унаследованного поля. Чтобы справиться с такими случаями, вы можете написать declare , чтобы указать TypeScript, что для этого объявления поля не должно быть никакого эффекта во время выполнения.

interface Animal {
  dateOfBirth: any;
}
 
interface Dog extends Animal {
  breed: any;
}
 
class AnimalHouse {
  resident: Animal;
  constructor(animal: Animal) {
    this.resident = animal;
  }
}
 
class DogHouse extends AnimalHouse {
  // Does not emit JavaScript code,
  // only ensures the types are correct
  declare resident: Dog;
  constructor(dog: Dog) {
    super(dog);
  }
}

Порядок инициализации

Порядок инициализации классов JavaScript в некоторых случаях может быть неожиданным. Рассмотрим этот код:

class Base {
  name = "base";
  constructor() {
    console.log("My name is " + this.name);
  }
}
 
class Derived extends Base {
  name = "derived";
}
 
// Prints "base", not "derived"
const d = new Derived();

Что произошло?

Порядок инициализации класса, как определено в JavaScript:

  • Инициализируются поля базового класса
  • Выполняется конструктор базового класса
  • Инициализируются поля производного класса
  • Выполняется конструктор производного класса

Это означает, что конструктор базового класса видел своё собственное значение для name во время собственного конструктора, так как инициализации полей производного класса ещё не были выполнены.

Наследование встроенных типов

Примечание: Если вы не планируете наследоваться от встроенных типов, таких как Array, Error, Map, и т. д., или если целевая компиляция явно установлена в ES6/ES2015 или выше, этот раздел можно пропустить

В ES2015 конструкторы, которые возвращают объект, неявно заменяют значение this для всех вызывающих сторон super(...). Необходимо, чтобы сгенерированный код конструктора захватывал любое потенциальное возвращаемое значение super(...) и заменял его на this.

В результате наследование от Error, Array и других может больше не работать должным образом. Это связано с тем, что функции-конструкторы для Error, Array и тому подобных используют new.target ECMAScript 6 для изменения цепочки прототипов; однако нет способа гарантировать значение для new.target при вызове конструктора в ECMAScript 5. Другие компиляторы с понижающей совместимостью, как правило, имеют то же ограничение по умолчанию.

Для подкласса, как в следующем примере:

class MsgError extends Error {
  constructor(m: string) {
    super(m);
  }
  sayHello() {
    return "hello " + this.message;
  }
}

вы можете обнаружить:

  • методы могут быть undefined на объектах, возвращаемых при создании этих подклассов, поэтому вызов sayHello приведет к ошибке.
  • instanceof будет нарушена между экземплярами подкласса и их экземплярами, поэтому (new MsgError()) instanceof MsgError вернет false.

В качестве рекомендации, вы можете вручную скорректировать прототип сразу после любых вызовов super(...).

class MsgError extends Error {
  constructor(m: string) {
    super(m);
 
    // Set the prototype explicitly.
    Object.setPrototypeOf(this, MsgError.prototype);
  }
 
  sayHello() {
    return "hello " + this.message;
  }
}

Однако любой подкласс MsgError должен также вручную установить прототип. Для сред выполнения, которые не поддерживают Object.setPrototypeOf, вы можете вместо этого использовать __proto__.

К сожалению, эти обходные пути не будут работать в Internet Explorer 10 и более ранних версиях. Можно вручную скопировать методы из прототипа в сам экземпляр (т. е. MsgError.prototype в this), но саму цепочку прототипов исправить нельзя.

Видимость членов

Вы можете использовать TypeScript для управления тем, какие методы или свойства видны коду за пределами класса.

public

По умолчанию видимость членов класса — public. Член public может быть доступен в любом месте:

class Greeter {
  public greet() {
    console.log("hi!");
  }
}
const g = new Greeter();
g.greet();

Поскольку public уже является модификатором видимости по умолчанию, вы никогда не обязаны писать его для члена класса, но можете выбрать это для повышения стиля/читабельности.

protected

Члены с protected видимостью доступны только подклассам класса, в котором они объявлены.

class Greeter {
  public greet() {
    console.log("Hello, " + this.getName());
  }
  protected getName() {
    return "hi";
  }
}
 
class SpecialGreeter extends Greeter {
  public howdy() {
    // OK to access protected member here
    console.log("Howdy, " + this.getName());
  }
}
const g = new SpecialGreeter();
g.greet(); // OK
g.getName();

Доступ к членам с protected видимостью

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

class Base {
  protected m = 10;
}
class Derived extends Base {
  // No modifier, so default is 'public'
  m = 15;
}
const d = new Derived();
console.log(d.m); // OK

Обратите внимание, что Derived уже мог свободно читать и записывать m, поэтому это не существенно изменяет «безопасность» этой ситуации. Главное, что нужно отметить, это то, что в производном классе нам нужно быть внимательными, чтобы повторить модификатор protected , если эта экспозиция не является намеренной.

Внутрииерархический protected доступ

Разные языки ООП не соглашаются в вопросе о том, законно ли обращение к члену с protected видимостью через ссылку на базовый класс:

class Base {
  protected x: number = 1;
}
class Derived1 extends Base {
  protected x: number = 5;
}
class Derived2 extends Base {
  f1(other: Derived2) {
    other.x = 10;
  }
  f2(other: Base) {
    other.x = 10;
  }
}

Например, Java считает это законным. С другой стороны, C# и C++ выбрали, что этот код должен быть незаконным.

TypeScript поддерживает C# и C++, так как доступ к x в Derived2 должен быть законным только из подклассов Derived2, а Derived1 таковым не является. Более того, если доступ к x через ссылку на Derived1 является незаконным (а это определенно должно быть так!), то доступ к нему через ссылку на базовый класс никогда не улучшит ситуацию.

См. также Почему нельзя получить доступ к защищенному члену из производного класса?, который более подробно объясняет обоснование C#.

private

private подобно protected, но не позволяет получить доступ к члену даже из подклассов:

class Base {
  private x = 0;
}
const b = new Base();
// Can't access from outside the class
console.log(b.x);
class Derived extends Base {
  showX() {
    // Can't access in subclasses
    console.log(this.x);
  }
}

Поскольку к членам с private видимостью нет доступа из производных классов, производный класс не может увеличить их видимость:

class Base {
  private x = 0;
}
class Derived extends Base {
  x = 1;
}

Межэкземплярный private доступ

Разные языки ООП не соглашаются в вопросе о том, могут ли разные экземпляры одного класса получать доступ к членам друг друга с private видимостью. В то время как такие языки, как Java, C#, C++, Swift и PHP разрешают это, Ruby — нет.

TypeScript позволяет межэкземплярный private доступ:

class A {
  private x = 10;
 
  public sameAs(other: A) {
    // No error
    return other.x === this.x;
  }
}

Ограничения

Как и другие аспекты системы типов TypeScript, private и protected действуют только во время проверки типов.

Это означает, что конструкции среды выполнения JavaScript, такие как in или простое обращение к свойству, все равно могут получить доступ к члену с private или protected видимостью:

class MySafe {
  private secretKey = 12345;
}
// In a JavaScript file...
const s = new MySafe();
// Will print 12345
console.log(s.secretKey);

private также позволяет обращаться с помощью квадратных скобок во время проверки типов. Это делает поля, объявленные как private, потенциально более простыми для доступа, например, в юнит-тестах, с недостатком, что эти поля являются «мягко-приватными» и не строго обеспечивают конфиденциальность.

class MySafe {
  private secretKey = 12345;
}
 
const s = new MySafe();
 
// Not allowed during type checking
console.log(s.secretKey);
 
// OK
console.log(s["secretKey"]);

В отличие от private TypeScript, частные поля JavaScript (#) остаются приватными после компиляции и не предоставляют ранее упомянутых лазеек, таких как доступ с помощью квадратных скобок, что делает их «жестко-приватными».

class Dog {
  #barkAmount = 0;
  personality = "happy";
 
  constructor() {}
}
"use strict";
class Dog {
    #barkAmount = 0;
    personality = "happy";
    constructor() { }
}
 

При компиляции в ES2021 или менее позднюю версию TypeScript будет использовать WeakMaps вместо #.

"use strict";
var _Dog_barkAmount;
class Dog {
    constructor() {
        _Dog_barkAmount.set(this, 0);
        this.personality = "happy";
    }
}
_Dog_barkAmount = new WeakMap();
 

Если вам нужно защитить значения в вашем классе от злоумышленников, вы должны использовать механизмы, которые предлагают жесткую конфиденциальность во время выполнения, такие как замыкания, WeakMaps или приватные поля. Обратите внимание, что эти дополнительные проверки конфиденциальности во время выполнения могут повлиять на производительность.

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

Дополнительная информация:
Статические члены (MDN)

Классы могут иметь static члены. Эти члены не связаны с конкретным экземпляром класса. К ним можно получить доступ через сам объект конструктора класса:

class MyClass {
  static x = 0;
  static printX() {
    console.log(MyClass.x);
  }
}
console.log(MyClass.x);
MyClass.printX();

Статические члены также могут использовать те же модификаторы видимости public, protected и private:

class MyClass {
  private static x = 0;
}
console.log(MyClass.x);

Статические члены также наследуются:

class Base {
  static getGreeting() {
    return "Hello world";
  }
}
class Derived extends Base {
  myGreeting = Derived.getGreeting();
}

Специальные статические имена

В целом не безопасно/невозможно перезаписывать свойства из прототипа Function. Поскольку классы сами являются функциями, которые могут быть вызваны с new, определённые static имена нельзя использовать. Функциональные свойства, такие как name, length и call не являются допустимыми для определения в качестве static членов:

class S {
  static name = "S!";
}

Почему нет статических классов?

TypeScript (и JavaScript) не имеют конструкции, называемой static class, так же, как, например, C#.

Эти конструкции существуют только потому, что эти языки вынуждают весь данные и функции находиться внутри класса; поскольку это ограничение в TypeScript отсутствует, они не нужны. Класс с единственным экземпляром обычно просто представляется как обычный объект в JavaScript/TypeScript.

Например, нам не нужна синтаксическая конструкция «статический класс» в TypeScript, потому что обычный объект (или даже функция верхнего уровня) сделает то же самое:

// Unnecessary "static" class
class MyStaticClass {
  static doSomething() {}
}
 
// Preferred (alternative 1)
function doSomething() {}
 
// Preferred (alternative 2)
const MyHelperObject = {
  dosomething() {},
};

static блоки в классах

Статические блоки позволяют написать последовательность операторов со своим собственным пространством имён, которые могут получать доступ к приватным полям внутри содержащего класса. Это означает, что мы можем написать код инициализации со всеми возможностями написания операторов, без утечки переменных и с полным доступом к внутренним частям нашего класса.

class Foo {
    static #count = 0;
 
    get count() {
        return Foo.#count;
    }
 
    static {
        try {
            const lastInstances = loadLastInstances();
            Foo.#count += lastInstances.length;
        }
        catch {}
    }
}

Обобщённые классы

Классы, подобно интерфейсам, могут быть обобщёнными. При создании экземпляра обобщённого класса с new, его параметры типа выводятся так же, как и в вызове функции:

class Box<Type> {
  contents: Type;
  constructor(value: Type) {
    this.contents = value;
  }
}
 
const b = new Box("hello!");

Классы могут использовать обобщающие ограничения и значения по умолчанию так же, как и интерфейсы.

Параметры типа в статических членах

Этот код некорректен, и это может быть не очевидно почему:

class Box<Type> {
  static defaultValue: Type;
}

Помните, что типы всегда полностью стираются! Во время выполнения существует только один слот свойства Box.defaultValue. Это означает, что установка Box<string>.defaultValue (если это было возможно) также изменит Box<number>.defaultValue — это не хорошо. static члены обобщённого класса никогда не могут ссылаться на параметры типа класса.

this во время выполнения в классах

Дополнительная информация:
Ключевое слово this (MDN)

Важно помнить, что TypeScript не изменяет поведение JavaScript во время выполнения, и JavaScript довольно известен своими странными поведенческими особенностями во время выполнения.

Обработка JavaScript this действительно необычна:

class MyClass {
  name = "MyClass";
  getName() {
    return this.name;
  }
}
const c = new MyClass();
const obj = {
  name: "obj",
  getName: c.getName,
};
 
// Prints "obj", not "MyClass"
console.log(obj.getName());

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

Это редко то, что вы хотите! TypeScript предоставляет некоторые способы смягчить или предотвратить подобные ошибки.

Стрелочные функции

Дополнительное чтение:
Стрелочные функции (MDN)

Если у вас есть функция, которая часто вызывается способом, теряющим её this контекст, может быть целесообразно использовать свойство стрелочной функции вместо определения метода:

class MyClass {
  name = "MyClass";
  getName = () => {
    return this.name;
  };
}
const c = new MyClass();
const g = c.getName;
// Prints "MyClass" instead of crashing
console.log(g());

Это имеет свои компромиссы:

  • Значение this гарантированно будет правильным во время выполнения, даже для кода, не проверенного с помощью TypeScript
  • Это потребует больше памяти, так как каждый экземпляр класса будет иметь свою собственную копию каждой функции, определенной таким образом
  • Вы не можете использовать super.getName в производном классе, потому что в цепочке прототипов нет записи для получения метода базового класса

this параметры

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

// TypeScript input with 'this' parameter
function fn(this: SomeType, x: number) {
  /* ... */
}
// JavaScript output
function fn(x) {
  /* ... */
}

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

class MyClass {
  name = "MyClass";
  getName(this: MyClass) {
    return this.name;
  }
}
const c = new MyClass();
// OK
c.getName();
 
// Error, would crash
const g = c.getName;
console.log(g());

Этот метод имеет обратные компромиссы по сравнению с подходом стрелочной функции:

  • JavaScript-вызывающие стороны все еще могут неправильно использовать метод класса, не осознавая это
  • Только одна функция на определение класса выделяется, а не по одному экземпляру класса
  • Определения базовых методов все еще могут быть вызваны через super.

this Типы

В классах специальный тип, называемый this, динамически ссылается на тип текущего класса. Давайте посмотрим, как это полезно:

class Box {
  contents: string = "";
  set(value: string) {
    this.contents = value;
    return this;
  }
}

Здесь TypeScript определил тип возвращаемого значения set как this, а не как Box. Теперь давайте создадим подкласс Box:

class ClearableBox extends Box {
  clear() {
    this.contents = "";
  }
}
 
const a = new ClearableBox();
const b = a.set("hello");

Вы также можете использовать this в аннотации типа параметра:

class Box {
  content: string = "";
  sameAs(other: this) {
    return other.content === this.content;
  }
}

Это отличается от написания other: Box — если у вас есть производный класс, его метод sameAs теперь будет принимать только другие экземпляры этого же производного класса:

class Box {
  content: string = "";
  sameAs(other: this) {
    return other.content === this.content;
  }
}
 
class DerivedBox extends Box {
  otherContent: string = "?";
}
 
const base = new Box();
const derived = new DerivedBox();
derived.sameAs(base);

this-основанные проверки типа

Вы можете использовать this is Type в позиции возврата для методов в классах и интерфейсах. При смешении с сужением типа (например, операторами if) тип целевого объекта сузится до указанного типа Type.

class FileSystemObject {
  isFile(): this is FileRep {
    return this instanceof FileRep;
  }
  isDirectory(): this is Directory {
    return this instanceof Directory;
  }
  isNetworked(): this is Networked & this {
    return this.networked;
  }
  constructor(public path: string, private networked: boolean) {}
}
 
class FileRep extends FileSystemObject {
  constructor(path: string, public content: string) {
    super(path, false);
  }
}
 
class Directory extends FileSystemObject {
  children: FileSystemObject[];
}
 
interface Networked {
  host: string;
}
 
const fso: FileSystemObject = new FileRep("foo/bar.txt", "foo");
 
if (fso.isFile()) {
  fso.content;
} else if (fso.isDirectory()) {
  fso.children;
} else if (fso.isNetworked()) {
  fso.host;
}

Распространенным случаем использования проверки типа, основанной на this, является возможность ленивой проверки конкретного поля. Например, этот случай удаляет undefined из значения, хранящегося в коробке, когда hasValue было проверено как true:

class Box<T> {
  value?: T;
 
  hasValue(): this is { value: T } {
    return this.value !== undefined;
  }
}
 
const box = new Box();
box.value = "Gameboy";
 
box.value;
 
if (box.hasValue()) {
  box.value;
}

Параметры-свойства

TypeScript предлагает специальную синтаксическую конструкцию для преобразования параметра конструктора в свойство класса с тем же именем и значением. Они называются параметрами-свойствами и создаются путем добавления одного из модификаторов видимости public, private, protected, или readonly перед аргументом конструктора. Результирующее поле получает эти модификатор(ы):

class Params {
  constructor(
    public readonly x: number,
    protected y: number,
    private z: number
  ) {
    // No body necessary
  }
}
const a = new Params(1, 2, 3);
console.log(a.x);
console.log(a.z);

Выражения классов

Дополнительное чтение:
Выражения классов (MDN)

Выражения классов очень похожи на объявления классов. Единственное существенное различие состоит в том, что выражениям классов не требуется имя, хотя мы можем ссылаться на них через любой идентификатор, к которому они были привязаны:

const someClass = class<Type> {
  content: Type;
  constructor(value: Type) {
    this.content = value;
  }
};
 
const m = new someClass("Hello, world");

abstract Классы и члены

Классы, методы и поля в TypeScript могут быть абстрактными.

Абстрактный метод или абстрактное поле — это тот, для которого не было предоставлено реализации. Эти члены должны присутствовать в абстрактном классе, который не может быть непосредственно инстанцирован.

Роль абстрактных классов — служить базовыми классами для подклассов, которые реализуют все абстрактные члены. Когда класс не имеет абстрактных членов, он считается конкретным.

Давайте рассмотрим пример:

abstract class Base {
  abstract getName(): string;
 
  printName() {
    console.log("Hello, " + this.getName());
  }
}
 
const b = new Base();

Мы не можем инстанцировать Base с new, потому что он абстрактный. Вместо этого нам нужно создать производный класс и реализовать абстрактные члены:

class Derived extends Base {
  getName() {
    return "world";
  }
}
 
const d = new Derived();
d.printName();

Обратите внимание, что если мы забудем реализовать абстрактные члены базового класса, получим ошибку:

class Derived extends Base {
  // forgot to do anything
}

Абстрактные конструкторские подписи

Иногда вам нужно принять функцию конструктора класса, которая создаёт экземпляр класса, наследуемого от некоторого абстрактного класса.

Например, вы можете написать такой код:

function greet(ctor: typeof Base) {
  const instance = new ctor();
  instance.printName();
}

TypeScript правильно сообщает вам, что вы пытаетесь инстанцировать абстрактный класс. В конце концов, учитывая определение greet, совершенно законно написать этот код, который приведет к созданию экземпляра абстрактного класса:

// Bad!
greet(Base);

Вместо этого вы хотите написать функцию, которая принимает что-то с конструкторской подписью:

function greet(ctor: new () => Base) {
  const instance = new ctor();
  instance.printName();
}
greet(Derived);
greet(Base);

Теперь TypeScript правильно сообщает вам о том, какие функции конструкторов классов могут быть вызваны — Derived может, потому что он конкретный, но Base не может.

Взаимоотношения между классами

В большинстве случаев классы в TypeScript сравниваются структурно, так же как и другие типы.

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

class Point1 {
  x = 0;
  y = 0;
}
 
class Point2 {
  x = 0;
  y = 0;
}
 
// OK
const p: Point1 = new Point2();

Аналогично, отношения подтипов между классами существуют даже если нет явного наследования:

class Person {
  name: string;
  age: number;
}
 
class Employee {
  name: string;
  age: number;
  salary: number;
}
 
// OK
const p: Person = new Employee();

Это звучит просто, но есть несколько случаев, которые кажутся более странными, чем другие.

Пустые классы не имеют членов. В системе структурной типизации тип без членов обычно является супертипом чего угодно другого. Так что если вы напишете пустой класс (не надо!), любой тип может быть использован вместо него:

class Empty {}
 
function fn(x: Empty) {
  // can't do anything with 'x', so I won't
}
 
// All OK!
fn(window);
fn({});
fn(fn);

© 2012-2023 Microsoft
Licensed under the Apache License, Version 2.0.
https://www.typescriptlang.org/docs/handbook/2/classes.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API