Всем привет! Меня зовут Лихопой Кирилл и я - frontend-разработчик в компании Idaproject.

В этой серии статей говорим об основах TypeScript, его преимуществах и фишках. Сегодня познакомимся со ссылочными типами данных в TypeScript. Если вы пропустили первую часть, то советую ознакомиться: Изучение TypeScript — Полное руководство для начинающих. Часть 1 — введение и примитивные типы данных. Итак, начнем.

Ссылочные типы

В JavaScript практически все - это объекты . На самом деле (и это может смутить), строки, числа и логические переменные могут быть объектами, если объявлять их с ключевым словом new :

let firstname = new String('Денис');
console.log(firstname); // String {'Денис'}

Но когда мы говорим о ссылочных типах в JavaScript, мы говорим о массивах, объектах и функциях.

Внимание: примитивы VS ссылочные типы

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

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

Если у нас есть две переменных x и y, и они обе содержат примитивное значение, то они полностью независимы друг от друга:

Обе переменных х и y хранят независимые примитивные данные
Обе переменных х и y хранят независимые примитивные данные
let x = 2;
let  y = 1;
x = y;
y = 100;
console.log(x); //1 (даже когда y присвоили 100, x все еще равняется 1

В случае со ссылочными типами все немного по-другому. Ссылочные типы ссылаются на место в памяти, где хранится объект, поэтому они так и называются.

point 1 и point 2 хранят ссылки на адрес, по которому хранится объект Point.
point 1 и point 2 хранят ссылки на адрес, по которому хранится объект Point.
let point1 = { x: 1,y: 1 };
let point2 = point1;
point1.y = 100;

console.log(point2.y); // 100 (point1 и point2 ссылаются на одно и то же место в памяти, где хранится объект)

Это было быстрое объяснение разницы между примитивами и ссылочными типами. Можете почитать эту статью, если хотите узнать подробнее: Примитивы vs ссылочные типы.

Массивы в TypeScript

В TypeScript вы можете указать, какой тип данных может содержать в себе массив:

let ids: number[] = [1, 2, 3, 4, 5]; // может содержать только цифры

let names: string[] = ['Денис', 'Аня', 'Богдан']; // может содержать только строки

let options: boolean[] = [true, false, false]; // может содержать true или false

let books: object[] = [
  { name: 'Алиса в Стране чудес', author: 'Льюис Кэррол' },
  { name: 'Идиот', author: 'Федор Достоевский' },
]; // может содержать только объекты

let arr: any[] = ['привет', 1, true]; // превращает TypeScript в JavaScript (об этом чуть позже)

ids.push(6);
ids.push('7'); // ОШИБКА: Аргумент типа 'string' не может быть присвоен параметру типа 'number'

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

let person: (string | number | boolean)[] = ['Денис', 1, true];
person[0] = 100;
person[1] = {name: 'Денис'} // Ошибка - массив person не может содержать в себе объекты 

Если вы иницализируете переменную со значением, то нет необходимсти указывать типы - TypeScript сделает это сам;

let person = ['Денис', 1, true]; // это идентично примеру выше
person[0] = 100;
person[1] = {name: 'Денис'} // Ошибка - массив person не может содержать в себе объекты

В TypeScript вы можете объявить специальный тип массива - кортеж. Кортеж - это массив с фиксированным размером и известным набором данных. Он более строгий, чем обычные массивы.

let person: [string, number, boolean] = ['Денис', 1, true];
person[0] = 100; // Ошибка - значение на 0 позции может быть только строкой

Объекты в TypeScript

Объекты в TypeScript должны содержать все объявленные свойства с теми же типами, которые были объявлены:

// Объявляем переменную-объект person со специальной аннотацией типов
let person: {
  name: string;
  location: string;
  isProgrammer: boolean;
};

// Присваиваем переменной person объект со всеми необходимыми полями и значениями
person = {
  name: 'Денис',
  location: 'RU',
  isProgrammer: true,
};

person.isProgrammer = 'Да'; // ОШИБКА: должно быть логическое значение

person = {
  name: 'Олег',
  location: 'RU',
};
// ОШИБКА: пропущено свойство isProgrammer

Для объявления подписи (некого “шаблона”) объекта вы можете использовать interface :

interface Person {
  name: string;
  location: string;
  isProgrammer: boolean;
}

let person1: Person = {
  name: 'Денис',
  location: 'RU',
  isProgrammer: true,
};

let person2: Person = {
  name: 'Саша',
  location: 'Россия',
  isProgrammer: false,
};

Мы также можем объявлять свойства функции c подписью функции. Это можно сделать как с классической функцией JavaScript (sayHi), так и со стрелочной функции (sayBye) из стандарта ES6 (о самих функциях в TypeScript мы поговорим уже в следующей главе).

interface Speech {
  sayHi(name: string): string;
  sayBye: (name: string) => string;
}

let sayStuff: Speech = {
  sayHi: function (name: string) {
    return Привет, ${name};
  },
  sayBye: (name: string) => Пока, ${name},
};

console.log(sayStuff.sayHi('Питер')); // Привет, Питер
console.log(sayStuff.sayBye('Питер')); // Пока, Питер

Функции в TypeScript

Мы можем объявить, какие типы должны быть у аргументов функции и какой тип должен быть у возвращаемого из функции значения.

// Объявляем функцию circle, которая будет принимать числовую переменную diam и возвращать строку
function circle(diam: number): string {
	return 'Длина окружности: ' + Math.PI * diam;
}

console.log(circle(10)); // Длина окружности: 31.41592653589793

То же самое, но со стрелочной функцией ES6:

const circle = (diam: number): string => {
	return 'Длина окружности: ' + Math.PI * diam;
}

console.log(circle(10)); // Длина окружности: 31.41592653589793

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

// Явное объявление функции
const circle: Function = (diam: number): string => {
	return 'Длина окружности: ' + Math.PI * diam;
};

// Неявное объявление функции - TypeScript видит, что circle - функция, которая всегда возвращает строку, так что это можно не объявлять
const circle = (diam: number) => {
return 'Длина окружности: ' + Math.PI * diam;
};

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

const add = (a: number, b: number, c?: number | string) => {
	console.log(c);
  return a + b;
};

console.log(add(5, 4, 'Здесь могло бы быть число, строка или вообще ничего!'));
// Здесь могло бы быть число, строка или вообще ничего!
// 9

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

const logMessage = (msg: string): void => {
	console.log('Логи: ' + msg);
};

logMessage('Typescript - это круто!');
// Логи: Typescript - это круто!

Если мы хотим объявить переменную-функцию, но не определять ее (обозначить, что она делает), то мы можем использовать подпись функции. В примере ниже функция sayHello должна соответстовать подписи после двоеточия:

// Объявим переменную sayHello и зададим ей подпись: принимает на вход строку и ничего не возвращает
let sayHello: (name: string) => void;

// Теперь определим функцию, соответствующую подписи
sayHello = (name) => {
	console.log('Привет, ' + name);
};

sayHello('Кирилл'); //Привет, Кирилл

Динамические типы (any)

Используя тип any , мы можем превратить TypeScript обратно в JavaScript:

let age: any = '100';
age = 100;
age = {
	years: 100,
	months: 2,
};

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

Псевдонимы типов

Псевдонимы типов помогут уменьшить повторение кода, держать его в соответствии с принципом DRY (Don’t Repeat Yourself - “не повторяйся” - прим. переводчика). Для создания псевдонима типов используется ключевое слово type . В примере ниже мы можем увидеть, что псевдоним типа personObject предотвращает повторение и выступает единственным источником истины для типов данных, которые должны содержаться в объекте person.

type StringOrNumber = string | number;

type PersonObject = {
  name: string;
  id: StringOrNumber;
};

const person1: PersonObject = {
  name: 'Федор',
  id: 1,
};

const person2: PersonObject = {
  name: 'Олег',
  id: 2,
};

const sayHello  = (person: PersonObject) => {
  return 'Привет, ' + person.name;
};

const sayGoodbye = (person: PersonObject) => {
  return 'Пока, ' + person.name;
};

DOM и приведение типов

В отличие от JavaScript, TypeScript не имеет доступа к DOM. Это означает, что при обращаении к DOM-элементам TypeScript не может быть уверен в том, что они существуют.

Иллюстрация этой проблемы - ниже:

const link = document.querySelector('a');
сonsole.log(link.href); //Ошибка: возможно объект является 'null'. TypeScript не может быть уверен в его существовании, т.к. у него нет доступа к DOM

С оператором ненулевого подтверждения ! мы можем сказать компилятору, что выражение не равно null или undefined . Это может быть полезным, когда компилятор не может узнать, какой тип используется, но мы знаем это.

// Здесь мы говорим TypeScript, что эта ссылка существует
const link = document.querySelector('a')!;
console.log(link.href); // habr.com

Обратите внимание, что нам не нужно объявлять тип переменной link . Как мы уже знаем, TypeScript сам понимает (с помощью определения типа), что эта переменная типа HTMLAnchorElement .

Но что, если нам надо найти DOM-элемент по его классу или id? TypeScript не может определит тип такой переменной, потому что она может быть чем угодно.

const form = document.getElementById('signup-form');

console.log(form.method);
// ОШИБКА: Возможно, объект является 'null'.
// ОШИБКА: У типа 'HTMLElement' нет свойства 'method'.

Выше мы получили две ошибки. Нам надо сообщить TypeScript, что мы уверены в том, что этот элемент существует, и что он типа HTMLFormElement . Для этого используется приведение типов (ключевое слово as):

const form = document.getElementById('signup-form') as HTMLFormElement;
console.log(form.method); // post

Теперь TypeScript счастлив!

TypeScript имеет встроенный объект Event . Если мы добавим просулшиватель событий на отправку нашей формы, то TypeScript выдаст нам ошибку при вызове какого-либо метода, который не является частью объекта Event . Посмотрите, насколько крут TypeScript - он может сообщить нам, что мы сделали орфографическую ошибку:

const form = document.getElementById('signup-form') as HTMLFormElement;

form.addEventListent('submit', (e: Event) => {
  console.log(e.tarrget); // ОШИБКА: Свойство 'tarrget' не существует у типа 'Event'. Может, вы имели ввиду 'target'?
});

В следущей части мы поговорим о классах, модулях и много чем еще, поэтому подписывайтесь!

А как вы считаете, стоит ли использовать TypeScript в работе, учитывая его возможности?

Комментарии (0)