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

Синтаксические конструкции и операторы

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

Операторы присваивания короткого замыкания (&&=, ||=, &&=)

В большинстве языков, в том числе и JavaScript, существует такое понятие как составные операторы присваивания (compound assignment operators), позволяющие совмещать операцию присваивания при помощи оператора = с какой-либо другой допустимой операцией (+-*/! и т.д.) и тем самым значительно сокращать выражения.

1
2
3
4
5
6
let a = 1;
let b = 2;

a += b; // тоже самое что a = a + b
a *= b; // тоже самое что a = a * b
// и т.д.

Множество существующих операторов совместимы с оператором = за исключением трех часто применяемых операторов, таких как логическое И (&&), логическое ИЛИ (||) и оператор нулевого слияния (??).

1
2
3
a = a && b;
a = a || b;
a = a ?? b;

Поскольку дополнительные синтаксические возможности лишь упрощают процесс разработки программ, благодаря комьюнити в TypeScript появился механизм, обозначаемый как операторы присваивания короткого замыкания. Данный механизм позволяет совмещать упомянутые ранее операторы &&, || и ?? непосредственно с оператором присваивания.

1
2
3
4
5
6
let a = {};
let b = {};

a &&= b; // a && (a = b)
a ||= b; // a || (a = b);
a ??= b; // a !== null && a !== void 0 ? a : (a = b);

Операнды для delete должны быть необязательными

Представьте случай, при котором в JavaScript коде вам необходимо удалить у объекта одно из трех определенных в нем полей.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
let o = {
  a: 0,
  b: '',
  c: true,
};

const f = (o) => delete o.b;

f(0); // удаляем поле b

Object.entries(o).forEach(([key, value]) =>
  console.log(key, value)
);
/**
 * log -
 * -> a, 0
 * -> b, true
 */

Задача предельно простая только с точки зрения динамической типизации JavaScript. С точки зрения статической типизации TypeScript, удаление члена объекта нарушает контракт, представляемый декларацией типа. Простыми словами, TypeScript не может гарантировать типобезопасность, пока не может гарантировать существование членов объекта, описанных в его типе.

 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
type O = {
  a: number;
  b: string;
  c: boolean;
};

let o: O = {
  a: 0,
  b: '',
  c: true,
};

const f = (o: O) => delete o.b; // [*]

f(o); // удаляем поле b

/**
 * [*] Error ->
 * Oбъект o больше не отвечает
 * типу O, поскольку в нем нет
 * обязательного поля b. Поэтому
 * если дальше по ходу выполнения
 * программы будут производиться
 * операции над удаленным полем,
 * то возникнет ошибка времени выполнения.
 */

Поэтому TypeScript позволяет удалять члены объекта при помощи оператора delete только в том случае, если они имеют тип any, unknown, never или объявлены как необязательные.

 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
type T0 = {
  field: any;
};

const f0 = (o: T0) => delete o.field; // Ok

type T1 = {
  field: unknown;
};

const f1 = (o: T1) => delete o.field; // Ok

type T2 = {
  field: never;
};

const f2 = (o: T2) => delete o.field; // Ok

type T3 = {
  field?: number;
};

const f3 = (o: T3) => delete o.field; // Ok

type T4 = {
  field: number;
};

const f4 = (o: T4) => delete o.field; // Error -> The operand of a 'delete' operator must be optional.

Объявление переменных 'необязательными' при деструктуризации массивоподобных объектов

При активном рекомендуемым флаге --noUnusedLocals компилятор выбрасывает ошибки, если переменные, объявленные при деструктуризации массивоподобных объектов, не задействованы в коде.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function getPlayerControlAll() {
  return [() => {}, () => {}];
}

/**
 * Где-то в другом файле
 */
function f() {
  /**
   * [*] Error -> 'stop' is declared but its value is never read.
   */
  let [stop /** [*] */, play] = getPlayerControlAll();

  return play;
}

Несмотря на то, что существует способ получать только необходимые значения, это не решит проблему семантики кода, поскольку идентификатор переменной является частью мозаики, иллюстрирующей работу логики. И хотя в TypeScript эту проблему можно решить и другими способами, они ничем не смогут помочь скомпилированному в JavaScript коду.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function getPlayerControlAll() {
  return [() => {}, () => {}];
}

/**
 * Где-то в другом файле
 */
function f() {
  /**
   * Ошибки больше нет, поскольку первый, неиспользуемый
   * элемент пропущен. Но, несмотря на это, семантически становится
   * непонятно, что же возвращает функция getPlayerControlAll().
   *
   * И несмотря на способы, способные решить проблему в TypeScript,
   * в скомпилированном виде от них не останется и следа.
   */
  let [, play] = getPlayerControlAll();

  return play;
}

Для таких случаев в TypeScript существует возможность при деструктуризации массивоподобных объектов объявлять переменные как - необязательные. Чтобы переменная расценивалась компилятором как необязательная, её идентификатор должен начинаться с нижнего подчёркивания _identifier.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function getPlayerControlAll() {
  return [() => {}, () => {}];
}

/**
 * Где-то в другом файле
 */
function f() {
  /**
   * [*] Ok -> несмотря на то, что переменная stop не
   * задействована в коде, ошибки не возникает, что позволяет
   * более глубоко понять логику кода.
   */
  let [_stop /** [*] */, play] = getPlayerControlAll();

  return play;
}

Модификатор abstract для описания типа конструктора

Абстрактные классы предназначены исключительно для расширения (невозможно создать его экземпляр с помощью оператора new), а его абстрактные члены должны обязательно должны быть переопределены потомками.

 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
/**
 * Абстрактный класс с одним абстрактным методом.
 */
abstract class Shape {
  abstract getRectangle(): ClientRect;
}

/**
 * Из-за того, что класс абстрактный, не получится создать его экземпляр с помощью оператора new.
 */
new Shape(); // Error -> Cannot create an instance of an abstract class.ts(2511)

/**
 * [0] Кроме этого, подкласс обязательно должен переопределять абстрактные члены своего суперкласса.
 */
class Circle extends Shape {
  getRectangle() {
    // [0]
    return {
      width: 0,
      height: 0,
      top: 0,
      right: 0,
      bottom: 0,
      left: 0,
    };
  }
}

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
interface IHasRectangle {
  getRectangle(): ClientRect;
}

type HasRectangleClass = new () => IHasRectangle;

/**
 * [*] Type 'typeof Shape' is not assignable to type 'HasRectangleClass'.
 Cannot assign an abstract constructor type to a non-abstract constructor type.ts(2322)
 */
let ClassType: HasRectangleClass = Shape; // Error [*]

Кроме этого, невозможно получить тип экземпляра абстрактного класса с помощью вспомогательного типа InstanceType<T>.

1
2
3
4
5
/**
 * [*] Type 'typeof Shape' does not satisfy the constraint 'new (...args: any) => any'.
  Cannot assign an abstract constructor type to a non-abstract constructor type.ts(2344)
 */
type Instance = InstanceType<typeof Shape>; // Error [*]

Это, в свою очередь, не позволяет реализовать механизм динамического наследования.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
function subclassCreator(Base: new () => IHasRectangle) {
  return class extends Base {
    getRectangle() {
      return {
        width: 0,
        height: 0,
        top: 0,
        right: 0,
        bottom: 0,
        left: 0,
      };
    }
  };
}

/**
 * [*] Argument of type 'typeof Shape' is not assignable to parameter of type 'new () => IHasRectangle'.
 Cannot assign an abstract constructor type to a non-abstract constructor type.ts(2345)
 */
subclassCreator(Shape); // Error [*] -> передача в качестве аргумента абстрактного класса
subclassCreator(Circle); // Ok -> передача в качестве аргумента конкретного класса

Для решения этой проблемы в TypeScript существует модификатор abstract, предназначенный для указания в описании типа конструктора.

1
2
3
4
5
6
7
8
interface IHasRectangle {
    getRectangle(): ClientRect;
}

type HasRectangleClass = abstract new() => IHasRectangle;


let ClassType: HasRectangleClass = Shape; // Ok

Несмотря на то, что тип класса имеет абстрактный модификатор, он также остается совместимым с типами конкретных классов.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function subclassCreator(Base: abstract new() => IHasRectangle){
    return class extends Base {
        getRectangle(){
            return {
                width:0,
                height: 0,
                top: 0,
                right: 0,
                bottom: 0,
                left: 0
            };
        }
    }
}

subclassCreator(Shape); // Ok -> абстрактный класс
subclassCreator(Circle); // Ok -> конкретный класс

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

1
2
3
type AbstractInstanceType<T extends abstract new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;

type Instance = AbstractInstanceType<typeof Shape>; // Ok

Комментарии