Синтаксические конструкции и операторы¶
Кроме типизации, TypeScript пытается сделать жизнь разработчиков более комфортной за счет добавления синтаксического сахара в виде операторов, не существующих в JavaScript мире. Помимо этого, текущая глава поведает о неоднозначных моментах, связанных с уже хорошо известными по JavaScript операторами.
Операторы присваивания короткого замыкания (&&=, ||=, &&=)¶
В большинстве языков, в том числе и JavaScript, существует такое понятие как составные операторы присваивания (compound assignment operators), позволяющие совмещать операцию присваивания при помощи оператора =
с какой-либо другой допустимой операцией (+-*/!
и т.д.) и тем самым значительно сокращать выражения.
let a = 1;
let b = 2;
a += b; // тоже самое что a = a + b
a *= b; // тоже самое что a = a * b
// и т.д.
Множество существующих операторов совместимы с оператором =
за исключением трех часто применяемых операторов, таких как логическое И (&&
), логическое ИЛИ (||
) и оператор нулевого слияния (??
).
a = a && b;
a = a || b;
a = a ?? b;
Поскольку дополнительные синтаксические возможности лишь упрощают процесс разработки программ, благодаря комьюнити в TypeScript появился механизм, обозначаемый как операторы присваивания короткого замыкания. Данный механизм позволяет совмещать упомянутые ранее операторы &&
, ||
и ??
непосредственно с оператором присваивания.
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 коде вам необходимо удалить у объекта одно из трех определенных в нем полей.
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 не может гарантировать типобезопасность, пока не может гарантировать существование членов объекта, описанных в его типе.
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
или объявлены как необязательные.
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
компилятор выбрасывает ошибки, если переменные, объявленные при деструктуризации массивоподобных объектов, не задействованы в коде.
function getPlayerControlAll() {
return [() => {}, () => {}];
}
/**
* Где-то в другом файле
*/
function f() {
/**
* [*] Error -> 'stop' is declared but its value is never read.
*/
let [stop /** [*] */, play] = getPlayerControlAll();
return play;
}
Несмотря на то, что существует способ получать только необходимые значения, это не решит проблему семантики кода, поскольку идентификатор переменной является частью мозаики, иллюстрирующей работу логики. И хотя в TypeScript эту проблему можно решить и другими способами, они ничем не смогут помочь скомпилированному в JavaScript коду.
function getPlayerControlAll() {
return [() => {}, () => {}];
}
/**
* Где-то в другом файле
*/
function f() {
/**
* Ошибки больше нет, поскольку первый, неиспользуемый
* элемент пропущен. Но, несмотря на это, семантически становится
* непонятно, что же возвращает функция getPlayerControlAll().
*
* И несмотря на способы, способные решить проблему в TypeScript,
* в скомпилированном виде от них не останется и следа.
*/
let [, play] = getPlayerControlAll();
return play;
}
Для таких случаев в TypeScript существует возможность при деструктуризации массивоподобных объектов объявлять переменные как - необязательные. Чтобы переменная расценивалась компилятором как необязательная, её идентификатор должен начинаться с нижнего подчёркивания _identifier
.
function getPlayerControlAll() {
return [() => {}, () => {}];
}
/**
* Где-то в другом файле
*/
function f() {
/**
* [*] Ok -> несмотря на то, что переменная stop не
* задействована в коде, ошибки не возникает, что позволяет
* более глубоко понять логику кода.
*/
let [_stop /** [*] */, play] = getPlayerControlAll();
return play;
}
Модификатор abstract для описания типа конструктора¶
Абстрактные классы предназначены исключительно для расширения (невозможно создать его экземпляр с помощью оператора new
), а его абстрактные члены должны обязательно должны быть переопределены потомками.
/**
* Абстрактный класс с одним абстрактным методом.
*/
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,
};
}
}
Но правила, с помощью которых компилятор работает с абстрактными классами, делают типы абстрактных и конкретных конструкторов несовместимыми. Другими словами, абстрактный класс нельзя передать по ссылке, ограниченной более общим типом.
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>
.
/**
* [*] 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 [*]
Это, в свою очередь, не позволяет реализовать механизм динамического наследования.
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
, предназначенный для указания в описании типа конструктора.
interface IHasRectangle {
getRectangle(): ClientRect;
}
type HasRectangleClass = abstract new() => IHasRectangle;
let ClassType: HasRectangleClass = Shape; // Ok
Несмотря на то, что тип класса имеет абстрактный модификатор, он также остается совместимым с типами конкретных классов.
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 -> конкретный класс
Также с помощью данного оператора можно реализовать собственный вспомогательный тип, позволяющий получить тип экземпляра.
type AbstractInstanceType<T extends abstract new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;
type Instance = AbstractInstanceType<typeof Shape>; // Ok