Перейти к содержанию

Классы — тонкости

В TypeScript с классами связано несколько неочевидных моментов. Неожиданная встреча с ними на практике непременно приведет к возникновению множества вопросов, ответы на которые содержит текущая глава.

Классы - тонкости implements

Кроме того, что класс может реализовать (implements) интерфейсы (interface), он также может реализовать другой класс.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
interface IAnimal {
  name: string;
}

class Animal {
  public name: string;
}

class Bird implements IAnimal {
  // Ok
  public name: string;
}
class Fish implements Animal {
  // Ok
  public name: string;
}

Как уже можно было догадаться, при реализации классом другого класса действуют те же правила, что и при расширении класса интерфейсом. То есть класс, у которого все члены объявлены как публичные (public), может реализовать любой другой класс. Если класс имеет определение членов с модификаторами доступа private или protected, то его может реализовать только этот же класс или его потомки.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Animal {
  public name: string;
}

class Bird implements Animal {
  // Ok
  public name: string;
  protected age: number;
}

class Fish implements Animal {
  // Ok
  public name: string;
  private arial: string;
}

class Raven implements Bird {
  // Error
  public name: string;
  protected age: number;
}

class Owl extends Bird implements Bird {
  // Ok
  public name: string;
  protected age: number;
}

class Shark implements Fish {
  // Error
  public name: string;
}

class Barracuda extends Fish implements Fish {
  // Ok
  public name: string;
}

Частичное слияние интерфейса с классом

На текущий момент известно, что два интерфейса, объявленные в одной области видимости, сливаются вместе. Кроме этого, если интерфейс объявлен в одной области видимости с одноимённым классом, то компилятор считает, что класс реализовал этот интерфейс.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
interface Animal {
  id: string;
  age: number;
}

class Animal {}

const animal = new Animal(); // Ok

animal.id = 'animal'; // Ok
animal.age = 0; // Ok

const { id, age } = animal; // Ok -> id: string and age: number

console.log(id, age); // 'animal', 0

Переопределение свойств полями и наоборот при наследовании

В JavaScript при использовании механизма наследования (extends) производный класс в состоянии переопределить свойство, объявленное в базовом классе полем и наоборот, поле свойством.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Base {
  get value() {
    return 'base';
  }
  set value(value) {
    console.log(value);
  }
}

class Derived extends Base {
  value = 'derived';
}

let derived = new Derived();

console.log(derived.value); // 'derived'

derived.value = `new derived`; // несложно догадаться, что при присваивании нового значения console.log в сеттер базового класса вызвана не будет

console.log(derived.value); // 'new derived'

/**
 * То же справедливо и для переопределения
 * поля, объявленного в базовом классе свойствами
 * производного класса.
 */

Но во избежание казусов, сопряженных с этим поведением, TypeScript запрещает переопределения при наследовании.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Base {
  get value() {
    return 'value';
  }
  set value(value: string) {}
}

class Derived extends Base {
  /**
   * Error ->
   *
   * 'value' is defined as an accessor in class 'Base',
   * but is overridden here in 'Derived'
   * as an instance property.
   */
  value = 'value';
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Base {
  value = 'value';
}

class Derived extends Base {
  /**
   * Error ->
   *
   * 'value' is defined as a property in class 'Base',
   * but is overridden here in 'Derived' as an accessor.
   */

  get value() {
    return 'value';
  }
  set value(value: string) {}
}

Комментарии