TypeScript в деталях. Часть 3

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.


Привет, друзья!


Представляю вашему вниманию перевод еще нескольких статей из серии 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. Capitilize<StringType>


Данная утилита конвертирует первый символ строкового литерального типа в верхний регистр:





15. Uncapitilize<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!




Источник: https://habr.com/ru/company/timeweb/blog/690726/


Интересные статьи

Интересные статьи

Пару лет назад, человек из Wrike написал серию статей про красную корпоративную культуру, причём во второй части буквально в 3 абзацах был весь смысл 4 статей. Было написано очень завуалировано и мягк...
Жесткие диски знакомы всем пользователям, без них сложно представить современный компьютер. Конечно, SSD вытеснили жесткие диски в сценариях, где требуется максимальная производительность, например, д...
Совсем недавно закончилась главная конференция мира виртуализации, VMworld 2021. Да, на сей раз без Пэта Гелсингера — на посту генерального директора его сменил Рагу Рагурам. Тем не менее, сессии полу...
22 июня автор курса «Разработчик C++» в Яндекс.Практикуме Георгий Осипов провёл вебинар «Вычисляем на видеокартах. Технология OpenCL». Мы подготовили для вас его текстовую версию, для удобства разб...
Первая часть проекта была посвящена используемым компонентам и вызвала довольно много комментариев и вопросов. В данной статье я подробно опишу процесс сборки и опишу при...