Пользовательское объединение типов - что это и как можно использовать

Помимо объединения примитивных типов данных (например):

type myType = number | string;

… в TypeScript можно делать и объединение пользовательских типов данных:

interface Rectange {
  width: number,
  height: number
}

interface Circle {
  radius: number
}

type Shape = Rectange | Circle;

Более того, пользовательские типы можно связать между собой при помощи общего для всех типов поля:

interface Rectange {
  width: number,
  height: number,
  type: 'rectange'
}

interface Circle {
  radius: number,
  type: 'circle'
}

type Shape = Rectange | Circle;

… здесь поле type - является таким связующим звеном; такое поле называется дискриминантом.

Прелесть такого объединения типов заключается в том, что можно проверять значение этого поля и в зависимости от результата - выполнять нужное действие. Например, можно создать такую функцию calcArea:

function calcArea(shape: Shape): number {
  const { type } = shape;

  switch(type) {
    case 'rectange':
      return shape.width * shape.height;
    case 'circle':
      return Math.round(Math.PI * shape.radius ** 2);
  }
}

… и тогда пример использования этой функции и объединенного типа будет таким:

interface Rectange {
  width: number,
  height: number,
  type: 'rectange'
}

interface Circle {
  radius: number,
  type: 'circle'
}

type Shape = Rectange | Circle;

function calcArea(shape: Shape): number {
  const { type } = shape;

  switch(type) {
    case 'rectange':
      return shape.width * shape.height;
    case 'circle':
      return Math.round(Math.PI * shape.radius ** 2);
  }
}

const rectangle: Rectange = { width: 10, height: 10, type: 'rectange' };
const circle: Circle = { radius: 20, type: 'circle' };

console.log(calcArea(rectangle)); // => 100
console.log(calcArea(circle)); // => 1257

Классы против интерфейсов в TypeScript - когда использовать, преимущества и недостатки

Классы

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

class Person {
  constructor(
    public name: string,
    public age: number
  ) {}
}

const person: Person = {
  name: 'John',
  age: 12
}

Однако, это не совсем правильный пример.

Преимуществом при использовании класса является тот факт, что можно создавать экземпляры этого класса:

class Person {
  constructor(
    public name: string,
    public age: number
  ) {}
}

const person: Person = new Person('John', 12);

Еще одно преимущество использования класса в TypeScript - возможность использования оператора instanceof:

class Person {
  constructor(
    public name: string,
    public age: number
  ) {}
}

const person: Person = new Person('John', 12);

if (person instanceof Person) {
  console.log('yes');
}

… в этом случае условие выполнится и проверка сработает, так как person является экземпляром класса Person.

Недостаток использования класса в TypeScript - при транспиляции в JavaScript в результирующем коде останется этот класс - или ввиде функции (ES5):

var Person = /** @class */ (function () {
  function Person(name, age) {
    this.name = name;
    this.age = age;
  }
  return Person;
}());
var person = new Person('John', 12);

… или ввиде класса (ES6):

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}
const person = new Person('John', 12);

Интерфейсы

Главное преимущество использования интерфейса в TypeScript - то, что в скомпилированном JS-коде не остается следов использования интерфейса; результирующий код - не утяжеляется:

"use strict";
const person = { name: 'John', age: 12 };
if (person instanceof Person) {
  console.log('yes');
}

… однако, использовать оператор instanceof в данном случае уже не получится - будет ошибка 'Person' only refers to a type, but is being used as a value here.

Есть задача - вставить в template Angular-приложения внешний js-скрипт

Пример номер один

Самый простой, правильный и надежный способ - добавить скрипт в сборку приложения.

Пошагово:

  1. создать папку scripts внутри директории assets - src/assets/scripts
  2. поместить js-скрипт внутрь созданной папки - src/assets/scripts/awesome.js
  3. открыть файл angular.json в корне проекта
  4. добавить путь к скрипту в ключе scripts:
{
  "projects": {
    "awesome-project": {
      "architect": 
            "scripts": ["src/assets/scripts/awesome.js,]
          },
          ....
}

Пример номер два

Есть еще способ добавления - при помощи Renderer2:

constructor(
  @Inject(DOCUMENT) private document: Document,
  private renderer2: Renderer2
) {}

ngOnInit(): void {
  const textScript = this.renderer2.createElement('script');
  textScript.src = 'https://code.jquery.com/jquery-3.3.1.slim.min.js';
  this.renderer2.appendChild(this.document.body, textScript);

  const srcScript = this.renderer2.createElement('script');
  srcScript.type = 'text/javascript';
  srcScript.text = `
    (function() {
      console.log('Hello from Siberia!')
    }());
  `;
  this.renderer2.appendChild(this.document.body, srcScript);
}

Пробовал этот способ - работает; но в консоли браузера появляется ошибка о необъявленной переменной X; отказался от этого способа.

Итог

Мне с первого раза помог способ номер один.

Что такое readonly в TypeScript

Если кратко - есть const - для создания констант в приложении. Объявили константу и один раз присвоили ей значение - все, больше изменить значение константы не получится - на то она и константа:

const abc: string = 'apple';
abc = 'melon';

… строка abc = 'melon' - будет с ошибкой - “Cannot assign to ‘abc’ because it is a constant”.

readonly - это аналог const, только - для полей объекта; объявили объект, указали у него свойство с readonly; один раз - присвоили этому полю значение - все, переприсвоить его уже не получится:

interface Person {
  readonly name: string,
  age: number
}

const person: Person = {
  name: 'John',
  age: 20
};

person.age = 21;
person.name = 'Mary';

… последняя строка - person.name = 'Mary' - в ней TypeScript выдаст ошибку - “Cannot assign to ‘name’ because it is a read-only property”.

Поле объекта с квалификатором readonly - неизменяемое свойство объекта.

Для меня немного запутанная картина с именованными областями отображения и главное - с правильной настройкой. Нужно немного прояснить для себя.

Первое - настройка маршрутов. В ключе outlet указываем имя дополнительной области outlet - aux:

const routes: Routes = [
  { path: '', redirectTo: 'home', pathMatch: 'full' },
  { path: 'home', component: FirstComponent },
  { path: 'chat', component: ChatComponent, outlet: 'aux' },
];

В шаблоне обозначаем разметку для дополнительной области отображения и указываем ее имя через атрибут name - какое указали и в маршрутах:

<div style="display: flex">
  <router-outlet></router-outlet>
  <router-outlet name="aux"></router-outlet>
</div>

В навигации в директиве routerLink - добавляем конфигурационный объект с ключом outlets. Значение этого ключа - объект с ключами primary и aux; эти ключи - имена областей отрисовки outlets;

имя primary - это имя по умолчанию; имя aux - это то имя дополнительной области отображения, которое указали в маршруте.

Самое неочевидное - это значения этих ключей. Значения - это path, указанный в настройках маршрута - path: ‘home’, path: ‘chat’.

<nav>
  <a [routerLink]="['/']">first</a>
  <a [routerLink]="['/second']">second</a>
  <a [routerLink]="[{ outlets: { primary: 'home', aux: 'chat' } }]">open chat</a>
  <a [routerLink]="[{ outlets: { aux: null } }]">close chat</a>
</nav>