
Привет, друзья!
Представляю вашему вниманию перевод еще нескольких статей из серии Mastering TypeScript, посвященных углубленному изучению TypeScript.
- Заметка о Mapped Types и других полезных возможностях современного TypeScript
 - TypeScript в деталях. Часть 1
 - TypeScript в деталях. Часть 2
 - Карманная книга по TypeScript
 - Шпаргалка по TypeScript
 
15 встроенных утилит типа
Утилиты типа (utility types) позволяют легко конвертировать, извлекать, исключать типы, получать параметры типов и типы значений, возвращаемых функциями.
1. Partial<Type>
Данная утилита делает все свойства Type опциональными (необязательными):

/**
 * Make all properties in T optional.
 * typescript/lib/lib.es5.d.ts
 */
type Partial<T> = {
    [P in keyof T]?: T[P];
};

2. Required<Type>
Данная утилита делает все свойства Type обязательными (она является противоположностью утилиты Partial):

/**
 * Make all properties in T required.
 * typescript/lib/lib.es5.d.ts
 */
type Required<T> = {
    [P in keyof T]-?: T[P];
};

3. Readonly<Type>
Данная утилита делает все свойства Type доступными только для чтения (readonly). Такие свойства являются иммутабельными (их значения нельзя изменять):

/**
 * Make all properties in T readonly.
 * typescript/lib/lib.es5.d.ts
 */
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

4. Record<Keys, Type>
Данная утилита создает новый объектный тип (object type), ключами которого являются Keys, а значениями свойств — Type. Эта утилита может использоваться для сопоставления свойств одного типа с другим типом:

/**
 * Construct a type with a set of properties K of type T.
 * typescript/lib/lib.es5.d.ts
 */
type Record<K extends keyof any, T> = {
    [P in K]: T;
};
5. Exclude<UnionType, ExcludedMembers>
Данная утилита создает новый тип посредством исключения из UnionType всех членов объединения, которые могут быть присвоены (assignable) ExcludedMembers:

/**
 * Exclude from T those types that are assignable to U.
 * typescript/lib/lib.es5.d.ts
 */
type Exclude<T, U> = T extends U ? never : T;

6. Extract<Type, Union>
Данная утилита создает новый тип посредством извлечения из Type всех членов объединения, которые могут быть присвоены Union:

/**
 * Extract from T those types that are assignable to U.
 * typescript/lib/lib.es5.d.ts
 */
type Extract<T, U> = T extends U ? T : never;

7. Pick<Type, Keys>
Данная утилита создает новый тип посредством извлечения из Type набора (множества) свойств Keys (Keys — строковый литерал или их объединение):

/**
 * From T, pick a set of properties whose keys are in the union K.
 * typescript/lib/lib.es5.d.ts
 */
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

8. Omit<Type, Keys>
Данная утилита создает новый тип посредством исключения из Type набора свойств Keys (Keys — строковый литерал или их объединение) (она является противоположностью утилиты Pick):

/**
 * Construct a type with the properties of T except for those
 * in type K.
 * typescript/lib/lib.es5.d.ts
 */
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

9. NonNullable<Type>
Данная утилита создает новый тип посредством исключения из Type значений null и undefined:

/**
 * Exclude null and undefined from T.
 * typescript/lib/lib.es5.d.ts
 */
type NonNullable<T> = T extends null | undefined ? never : T;
10. Parameters<Type>
Данная утилита создает кортеж (tuple) из типов параметров функции Type:

/**
 * Obtain the parameters of a function type in a tuple.
 * typescript/lib/lib.es5.d.ts
 */
type Parameters<T extends (...args: any) => any> = T extends
  (...args: infer P) => any ? P : never;
11. ReturnType<Type>
Данная утилита извлекает тип значения, возвращаемого функцией Type:

/**
 * Obtain the return type of a function type.
 * typescript/lib/lib.es5.d.ts
 */
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
12. Uppercase<StringType>
Данная утилита конвертирует строковый литеральный тип в верхний регистр:

13. Lowercase<StringType>
Данная утилита конвертирует строковый литеральный тип в нижний регистр:

14. Capitalize<StringType>
Данная утилита конвертирует первый символ строкового литерального типа в верхний регистр:

15. Uncapitalize<StringType>
Данная утилита конвертирует первый символ строкового литерального типа в нижний регистр:

Кроме описанных выше, существует еще несколько встроенных утилит типа:
- 
ConstructorParameters<Type>: создает кортеж или массив из конструктора функции (речь во всех случаях идет о типах). Результатом является кортеж всех параметров типа (или типnever, еслиTypeне является функцией); - 
InstanceType<Type>: создает тип, состоящий из типа экземпляра конструктора функции типаType: - 
ThisParameterType<Type>: извлекает тип из параметраthisфункции. Если функция не имеет такого параметра, возвращаетсяunknown. 
10 особенностей классов
В объектно-ориентированных языках программирования класс — это шаблон (blueprint — проект, схема), описывающий свойства и методы, которые являются общими для всех объектов, создаваемых с помощью класса.
1. Свойства и методы
1.1. Свойства экземпляров и статические свойства
В TS, как и в JS, класс определяется с помощью ключевого слова class:
class User {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
}
В приведенном примере определяется класс User с одним свойством экземпляров name. В действительности, класс — это синтаксический сахар для функции-конструктора. Если установить результат компиляции в ES5, то будет сгенерирован следующий код:
"use strict";
var User = /** @class */ (function () {
    function User(name) {
        this.name = name;
    }
    return User;
}());
Кроме свойств экземпляров, в классе могут определяться статические свойства. Такие свойства определяются с помощью ключевого слова static:
class User {
    static cid: string = "eft";
    name: string;
    constructor(name: string) {
        this.name = name;
    }
}
В чем разница между свойствами экземпляров и статическими свойствами? Посмотрим на компилируемый код:
"use strict";
var User = /** @class */ (function () {
    function User(name) {
        this.name = name;
    }
    User.cid = "eft";
    return User;
}());
Как видим, свойства экземпляров определяются в экземпляре класса, а статические свойства — в его конструкторе.
1.2. Методы экземпляров и статические методы
Кроме свойств, в классе могут определяться методы экземпляров и статические методы:
class User {
  static cid: string = "eft";
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  static printCid() {
    console.log(User.cid);
  }
  send(msg: string) {
    console.log(`${this.name} send a message: ${msg}`);
  }
}
В чем разница между методами экземпляров и статическими методами? Посмотрим на компилируемый код:
"use strict";
var User = /** @class */ (function () {
    function User(name) {
        this.name = name;
    }
    User.printCid = function () {
        console.log(User.cid);
    };
    User.prototype.send = function (msg) {
        console.log("".concat(this.name, " send a message: ").concat(msg));
    };
    User.cid = "eft";
    return User;
}());
Как видим, методы экземпляров добавляются в прототип конструктора, а статические методы в сам конструктор.
2. Аксессоры
В классе могут определяться так называемые аксессоры (accessors). Аксессоры, которые делятся на геттеры (getters) и сеттеры (setters) могут использоваться, например, для инкапсуляции данных или их верификации:
class User {
  private _age: number = 0;
  get age(): number {
    return this._age;
  }
  set age(value: number) {
    if (value > 0 && value <= 120) {
      this._age = value;
    } else {
      throw new Error("The set age value is invalid!");
    }
  }
}
3. Наследование
Наследование (inheritance) — это иерархическая модель для связывания классов между собой. Наследование — это возможность класса наследовать функционал другого класса и расширять его новым функционалом. Наследование — это наиболее распространенный вид отношений между классами, между классами и интерфейсами, а также между интерфейсами. Наследование облегчает повторное использование кода.
Наследование реализуется с помощью ключевого слова extends. Расширяемый класс называется базовым (base), а расширяющий — производным (derived). Производный класс содержит все свойства и методы базового и может определять дополнительные члены.
3.1. Базовый класс
class Person {
  constructor(public name: string) {}
  public say(words: string) :void {
    console.log(`${this.name} says:${words}`);
  }
}
3.2. Производный класс
class Developer extends Person {
  constructor(name: string) {
    super(name);
    this.say("Learn TypeScript")
  }
}
const bytefer = new Developer("Bytefer");
// "Bytefer says:Learn TypeScript"
Класс Developer расширяет (extends) класс Person. Следует отметить, что класс может расширять только один класс (множественное наследование в TS, как и в JS, запрещено):

Однако мы вполне можем реализовывать (implements) несколько интерфейсов:
interface CanSay {
  say(words: string) :void
}
interface CanWalk {
  walk(): void;
}
class Person implements CanSay, CanWalk {
  constructor(public name: string) {}
  public say(words: string) :void {
    console.log(`${this.name} says:${words}`);
  }
  public walk(): void {
    console.log(`${this.name} walk with feet`);
  }
}
Рассмотренные классы являются конкретными (concrete). В TS также существуют абстрактные (abstract) классы.
4. Абстрактные классы
Классы, поля и методы могут быть абстрактными. Класс, определенный с помощью ключевого слова abstract, является абстрактным. Абстрактные классы не позволяют создавать объекты (другими словами, они не могут инстанцироваться (instantiate) напрямую):

Абстрактный класс — это своего рода проект класса. Подклассы (subclasses) абстрактного класса должны реализовывать всех его абстрактных членов:
class Developer extends Person {
  constructor(name: string) {
    super(name);
  }
  say(words: string): void {
    console.log(`${this.name} says ${words}`);
  }
}
const bytefer = new Developer("Bytefer");
bytefer.say("I love TS!"); // Bytefer says I love TS!
5. Видимость членов
В TS для управления видимостью (visibility) свойств и методов класса применяются ключевые слова public, protected и private. Видимость означает возможность доступа к членам за пределами класса, в котором они определяются.
5.1. public
Дефолтной видимостью членов класса является public. Такие члены доступны за пределами класса без каких-либо ограничений:
class Person {
  constructor(public name: string) {}
  public say(words: string) :void {
    console.log(`${this.name} says:${words}`);
  }
}
5.2. protected
Такие члены являются защищенными. Это означает, что они доступны только в определяющем их классе, а также в производных от него классах:

class Developer extends Person {
  constructor(name: string) {
    super(name);
    console.log(`Base Class:${this.getClassName()}`);
  }
}
const bytefer = new Developer("Bytefer"); // "Base Class:Person"
5.3. private
Такие члены являются частными (приватными). Это означает, что они доступны только в определяющем их классе:

Обратите внимание: private не делает членов по-настоящему закрытыми. Это всего лишь соглашение (как префикс _ в JS). Посмотрим на компилируемый код:
"use strict";
var Person = /** @class */ (function () {
    function Person(id, name) {
        this.id = id;
        this.name = name;
    }
    return Person;
}());
var p1 = new Person(28, "bytefer");
5.4. Частные поля
Реальные закрытые поля поддерживаются в TS, начиная с версии 3.8 (а в JS — с прошлого года):

Посмотрим на компилируемый код:
"use strict";
var __classPrivateFieldSet = // игнорировать соответствующий код;
var _Person_name;
class Person {
    constructor(name) {
        _Person_name.set(this, void 0);
        __classPrivateFieldSet(this, _Person_name, name, "f");
    }
}
_Person_name = new WeakMap();
const bytefer = new Person("Bytefer");
Отличия между частными и обычными полями могут быть сведены к следующему:
- закрытые поля определяются с помощью префикса 
#; - областью видимости приватного поля является определяющий его класс;
 - в отношении частных полей не могут применяться модификаторы доступа (
publicи др.); - приватные поля недоступны за пределами определяющего их класса.
 
6. Выражение класса
Выражение класса (class expression) — это синтаксис, используемый дял определения классов. Как и функциональные выражения, выражения класса могут быть именованными и анонимными. В случае с именованными выражениями, название доступно только в теле класса.
Синтаксис выражений класса (квадратные скобки означают опциональность):
const MyClass = class [className] [extends] {
  // тело класса
};
Пример определения класса Point:
const Point = class {
  constructor(public x: number, public y: number) {}
  public length() {
    return Math.sqrt(this.x * this.x + this.y * this.y);
  }
}
const p = new Point(3, 4);
console.log(p.length()); // 5
При определении класса с помощью выражения также можно использовать ключевое слово extends.
7. Общий класс
Для определения общего (generic) класса используется синтаксис <T, ...> (параметры типа) после названия класса:
class Person<T> {
  constructor(
    public cid: T,
    public name: string
  ) {}
}
const p1 = new Person<number>(28, "Lolo");
const p2 = new Person<string>("eft", "Bytefer");
Рассмотрим пример инстанцирования p1:
- при создании объекта 
Personпередается типnumberи параметры конструктора; - в классе 
Personзначение переменной типаTстановится числом; - наконец, параметр типа свойства 
cidв конструкторе также становится числом. 
Случаи использования дженериков:
- интерфейс, функция или класс работают с несколькими типами данных;
 - в интерфейсе, функции или классе тип данных используется в нескольких местах.
 
8. Сигнатура конструктора
При определении интерфейса для описания конструктора может использоваться ключевое слово new:
interface Point {
  new (x: number, y: number): Point;
}
new (x: number, y: number) называется сигнатурой конструктора (construct signature). Она имеет следующий синтаксис:
ConstructSignature: new TypeParametersopt ( ParameterListopt ) TypeAnnotationopt
TypeParametersopt, ParameterListopt и TypeAnnotationopt — это опциональный параметр типа, опциональный список параметров и опциональная аннотация типов, соответственно. Как применяется сигнатура конструктора? Рассмотрим пример:
interface Point {
  new (x: number, y: number): Point;
  x: number;
  y: number;
}
class Point2D implements Point {
  readonly x: number;
  readonly y: number;
constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}
const point: Point = new Point2D(1, 2); // Error
Сообщение об ошибке выглядит так:
Type 'Point2D' is not assignable to type 'Point'.
 Type 'Point2D' provides no match for the signature 'new (x: number, y: number): Point'.ts(2322)
Для решения проблемы определенный ранее интерфейс Point нужно немного отрефакторить:
interface Point {
  x: number;
  y: number;
}
interface PointConstructor {
  new (x: number, y: number): Point;
}
Далее определяем фабричную функцию newPoint, которая используется для создания объекта Point, соответствующего конструктору входящего типа PointConstructor:
class Point2D implements Point {
  readonly x: number;
  readonly y: number;
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}
function newPoint(
  pointConstructor: PointConstructor,
  x: number,
  y: number
): Point {
  return new pointConstructor(x, y);
}
const point: Point = newPoint(Point2D, 3, 4);
9. Абстрактная сигнатура конструктора
Абстрактная сигнатура конструктора была представлена в TS 4.2 для решения таких проблем, как:
type Constructor = new (...args: any[]) => any;
abstract class Shape {
  abstract getArea(): number;
}
const Ctor: Constructor = Shape; // Error
// Type 'typeof Shape' is not assignable to type 'Constructor'.
//  Cannot assign an abstract constructor type to a non-abstract
// constructor type.ts(2322)
Как видим, тип абстрактного конструктора не может присваиваться типу реального конструктора. Для решения данной проблемы следует использовать абстрактную сигнатуру конструктора:
type AbstractConstructor = abstract new (...args: any[]) => any;
abstract class Shape {
  abstract getArea(): number;
}
const Ctor: AbstractConstructor = Shape; // Ok
Далее определяем функцию makeSubclassWithArea для создания подклассов класса Shape:
function makeSubclassWithArea(Ctor: AbstractConstructor) {
  return class extends Ctor {
    #sideLength: number;
    constructor(sideLength: number) {
      super();
      this.#sideLength = sideLength;
    }
    getArea() {
      return this.#sideLength ** 2;
    }
  };
}
const Square = makeSubclassWithArea(Shape);
Следует отметить, что типы реальных конструкторов типам абстрактных конструкторов присваивать можно:
abstract class Shape {
  abstract getArea(): number;
}
class Square extends Shape {
  #sideLength: number;
  constructor(sideLength: number) {
    super();
    this.#sideLength = sideLength;
  }
  getArea() {
    return this.#sideLength ** 2;
  }
}
const Ctor: AbstractConstructor = Shape; // Ok
const Ctor1: AbstractConstructor = Square; // Ok
В заключение кратко рассмотрим разницу между типом class и типом typeof class.
10. Тип class и тип typeof class

На основе результатов приведенного примера можно сделать следующие выводы:
- при использовании класса 
Personв качестве типа значение переменной ограничивается экземпляром этого класса; - при использовании 
typeof Personв качестве типа значение переменной ограничивается статическими свойствами и методами данного класса. 
Следует отметить, что в TS используется система структурированных типов (structured type system), которая отличается от системы номинальных типов (nominal type system), применяемой в Java/C++, поэтому следующий код в TS будет работать без каких-либо проблем:
class Person {
  constructor(public name: string) {}
}
class SuperMan {
  constructor(public name: string) {}
}
const s1: SuperMan = new Person("Bytefer"); // Ok
Определение объекта с неизвестными свойствами
Приходилось ли вам сталкиваться с подобной ошибкой?

Для решения данной проблемы можно прибегнуть к помощи типа any:
consr user: any = {}
user.id = "TS001";
user.name = "Bytefer";
Но такое решение не является безопасным с точки зрения типов и нивелирует преимущества использования TS.
Другим решением может быть использование type или interface:
interface User {
  id: string;
  name: string;
}
const user = {} as User;
user.id = "TS001";
user.name = "Bytefer";
Кажется, что задача решена, но что если мы попробует добавить в объект свойство age?
Property 'age' does not exist on type 'User'
Получаем сообщение об ошибке. Что делать? Когда известны типы ключей и значений, для определения типа объекта можно воспользоваться сигнатурой доступа по индексу (index signatures). Синтаксис данной сигнатуры выглядит так:

Обратите внимание: типом ключа может быть только строка, число, символ или строковый литерал. В свою очередь, значение может иметь любой тип.

Определяем тип User с помощью сигнатуры доступа по индексу:
interface User {
  id: string;
  name: string;
  [key: string]: string;
}
При использовании сигнатуры доступа по индексу можно столкнуться с такой ситуацией:

- Почему к соответствующему свойству можно получить доступ как с помощью строки 
"1", так и с помощью числа1? - Почему 
keyof NumbersNamesвозвращает объединение из строки и числа? 
Это объясняется тем, что JS неявно приводит число к строке при использовании первого в качестве ключа объекта. TS применяет такой же алгоритм.
Кроме сигнатуры доступа по индексу для определения типа объекта можно использовать встроенную утилиту типа Record. Назначение данной утилиты состоит в следующем:

type User = Record<string, string>
const user = {} as User;
user.id = "TS001"; // Ok
user.name = "Bytefer"; // Ok
В чем разница между сигнатурой доступа по индексу и утилитой Record? Они обе могут использоваться для определения типа объекта с неизвестными (динамическими) свойствами:
const user1: Record<string, string> = { name: "Bytefer" }; // Ok
const user2: { [key: string]: string } = { name: "Bytefer" }; // Ok
Однако в случае с сигнатурой тип ключа может быть только string, number, symbol или шаблонным литералом. В случае с Record ключ может быть литералом или их объединением:

Взглянем на внутреннюю реализацию Record:
/**
 * Construct a type with a set of properties K of type T.
 * typescript/lib/lib.es5.d.ts
 */
type Record<K extends keyof any, T> = {
    [P in K]: T;
};
Перегрузки функции
Знаете ли вы, почему на представленном ниже изображении имеется столько определений функции ref и зачем они нужны?

Рассмотрим пример простой функции logError, принимающей параметр строкового типа и выводящей сообщение об ошибке в консоль инструментов разработчика в браузере:
function logError(msg: string) {
  console.error(`Возникла ошибка: ${msg}`);
}
logError('Отсутствует обязательное поле.');
Что если мы хотим, чтобы данная функция также принимала несколько сообщений в виде массива?
Одним из возможных решений является использование объединения (union types):
function logError(msg: string | string[]) {
  if (typeof msg === 'string') {
    console.error(`Возникла ошибка: ${msg}`);
  } else if (Array.isArray(msg)) {
    console.error(`Возникли ошибки: ${msg.join('\n')}`);
  }
}
logError('Отсутствует обязательное поле.')
logError(['Отсутствует обязательное поле.', 'Пароль должен состоять минимум из 6 символов.'])
Другим решением является использование перегрузки функции (function overloading). Перегрузка функции предполагает наличие сигнатур перегрузки (overload signatures) и сигнатуры реализации (implementation signature).

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

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

После объединения сигнатур перегрузки и сигнатуры реализации мы имеет такую картину:

Обратите внимание: вызываются только сигнатуры перегрузки. При обработке перегрузки функции TS анализирует список перегрузок и пытается использовать первое определение. Если определение совпадает, анализ прекращается:

Если вызвать функцию с типом параметра, соответствующего сигнатуре реализации, возникнет ошибка:

Перегружаться могут не только функции, но и методы классов. Перегрузка метода — это техника, когда вызывается один и тот же метод класса, но с разными параметрами (разными типами параметров, разным количеством параметров, разным порядком параметров и т.д.). Конкретная сигнатура метода определяется в момент передачи реального параметра.
Рассмотрим пример перегрузки метода:
class Calculator {
  add(a: number, b: number): number;
  add(a: string, b: string): string;
  add(a: string, b: number): string;
  add(a: number, b: string): string;
  add(a: string | number, b: string | number) {
if (typeof a === 'string' || typeof b === 'string') {
    return a.toString() + b.toString();
  }
    return a + b;
  }
}
const calculator = new Calculator();
const result = calculator.add('Bytefer', ' likes TS');
Надеюсь, что вы, как и я, нашли для себя что-то интересное.
Благодарю за внимание и happy coding!
          
 