Front-end course

Types, Functions

Types

TypeScript имеет дополнительные концепции и типы данных, которых нет в JavaScript и мы с ними ранее не знакомились.

Tuples

Этот тип есть во многих других языках программирования, но в JavaScript у нас его нет. Это массив с фиксированной длинной и типами. Выглядит он как массив [2, ‘hello’]. Давайте посмотрим как с ним работать в коде

const person = {
  name: "John",
  age: 30,
  hobbies: ['Driving', 'Cooking'],
  data: [2, 'hello']
};

Да он выглядит как обычный массив у которого элементы разного типа. Но если честно мы еще не указали, что ключ data - tuple. Для этого еам необходимо самолично указать тип переменной person, как мы это делали ранее

const person: {
  name: string;
  age: number;
  hobbies: string[];
  data: [number, string];
} = {
  name: "John",
  age: 30,
  hobbies: ['Driving', 'Cooking'],
  data: [2, 'hello']
};

Посмотрите отличие, как мы указали тип ключу hobbies и data! В первом случае мы указали уже известный ранее тип для массива. Это массив строк, но во втором случае (ключ data) мы четко указываем какой тип данных должен быть на каком индексе и сколько будет элементов в массиве.

Будьте внимательны при работе с tuples. Дело в том, что такие методы работы с массивом как push, pop, shift, unshift не вызывают ошибку в TypeScript.

Enum

Как и с Tuple этого типа данных нет в JavaScript, но есть во многих других языках программирования.

Он применяется для работы с константами или наборами каких-то данных. Он read-only и возможность добавить в него данные только при создании. Представим, что нам нужно сделать указание ролей, что 0 это Admin, 1 это User, 2 это Клиент. В данном случае у нас есть два варианта: записывать в переменную числа или же другой вариант, записывать название роли. TypeScript же предоставляет удобный вариант работы с этим. Посмотрим через код на это:

const person = {
  name: "John",
  age: 30,
  hobbies: ['Driving', 'Cooking'],
  data: [2, 'hello'],
  role: 2
};

// or

const person = {
  name: "John",
  age: 30,
  hobbies: ['Driving', 'Cooking'],
  data: [2, 'hello'],
  role: 'CLIENT'
};

Можно конечно работать и с этим, но есть неудобство небольшое при таком использовании:

  • нужно помнить что значит 0, 1, 2 и не перепутать
  • нужно помнить как мы записали константу роли где-то там в коде

То и другое приведет к 100% ошибке рано или поздно. Как бы мы это сделали в JavaScript? Мы бы создали отдельно где-то константы и потом их просто использовали

const ADMIN = 0;
const USER = 1;
const CLIENT = 2;

Собственно Enum так и работает

enum Role {
  ADMIN,
  USER,
  CLIENT
}

Имена могут быть не только в верхнем регистре, это опционально.

Теперь чтобы использовать это достаточно обратиться к нашему enum и через точку указать, то значение которое хотим

enum Role {
  ADMIN,
  USER,
  CLIENT
}

const person = {
  name: "John",
  age: 30,
  hobbies: ['Driving', 'Cooking'],
  data: [2, 'hello'],
  role: Role.ADMIN
};

По дефолту enum проставляет нашим константам значения 0, 1, 2, 3,… , но мы можем саму указать что хранить в них. К примеру нам нужно хранить градусы или статусы публикации. Для того что бы указать значение в enum, достаточно просто присвоить значение через знак равно

enum Temperature {
  Low = 0,
  Middle = 40,
  Hot = 60
}

// Temperature.Low, Temperature.Middle, Temperature.Hot

enum Status {
  Published = 'published',
  Created = 'created',
  Deleted = 'deleted'
}

Any

Этот тип говорит сам о себе. Им мы модем указать, что тип переменной будет “любым”. Нужно стараться применять его очень редко, максимально редко! В настройках TypeScript даже есть настройка, чтобы не позволять писать any. Так что старайтесь его не использовать. Почему? Ответ простой - используя any вы лишаетесь всех преимуществ которые даёт вам TypeScript в работе с типами. Можно сказать вы его почти выключаете.

Когда бы его возможно использовать? К примеру когда вы работаете с данными которые меняются, или вы не можете предугадать какой тип вы получите и с чем будете работать. Но такое бывает редко. Если есть возможность описать тип, то описывайте! any для очень редких случаев.

Union Types

Для более гибкой работы с типами переменных есть такая вещь как union types. Они позволяют нам указывать что переменная может содержать не один тип, а несколько. Для указания (перечисления) типов достаточно поставить вертикальную линию между ними.

let data: number | string = 5;

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

function add(num1: number, num2: number): number | string {
  const result = num1 + num2;
  
  if (result % 2) {
    return `Result: ${result}`;
  } else {
    return result;
  }
  
}

Literal Types

Мы можем указать тип не просто number, string и т.д., а и указать типа как какое-то значение. Бывает очень удобно когда нам необходимо сделать так, что переменная или аргумент функции может иметь только несколько значений.

Вот пример прошлой функции, которая возвращает или число, или строку. Давайте применим тут литеральный тип.

function add(
  num1: number, 
  num2: number, 
  resultType: 'as-number' | 'as-string'
): number | string {
  const result = num1 + num2;
  
  if (resultType === 'as-string') {
    return `Result: ${result}`;
  } else {
    return result;
  }
  
}

В таком случае, мы можем передать resultType при вызове функции только строку ‘as-number’ или ‘as-string’. Мы тем самым ограничили возможные варианты значений которые может принять этот аргумент.

Type Aliases / Custom Types

Еще одна возможность которая есть в работе с типами, это создание своих кастомных типов, либо же объединение нескольких в удобную для работы форму.

Для того чтобы создать свой тип, необходимо воспользоваться ключевым словом type. Синтаксис довольно такие простой: type Name = our type. Давайте посмотрим на примере который писали ранее. У нас есть union и literal типы, перенесем их в отдельный тип.

type Result = number | string;
type ResultType = 'as-number' | 'as-string';

function add(
  num1: number, 
  num2: number, 
  resultType: ResultType
): Result {
  const result = num1 + num2;
  
  if (resultType === 'as-string') {
    return `Result: ${result}`;
  } else {
    return result;
  }
  
}

Как видите ничего сложно, ключевое слово type нам позволяет описать какой-то сложный тип или набор типов в одну переменную и использовать. Конечно можно таким образом записать базовый тип(number, string,…) в свой кастомный, но мы от этого ничего не выгадаем.

Очень удобно работать с элиасами при взаимодействии с объектами. Объекты могут иметь большую структуру и не особо удобно если бы нам приходилось постоянно переносить руками.

type User = { name: string; age: number; };
const admin: User = { name: 'Max', age: 30 };

или вот пример упрощения более наглядный

function greet(user: { name: string; age: number; }) {
  console.log(`Hi, I am ${user.name}`);
}

function isOlder(user: { name: string; age: number; }, checkAge: number) {
  return checkAge > user.age;
}

// to
type User = { name: string; age: number; };

function greet(user: User) {
  console.log(`Hi, I am ${user.name}`);
}

function isOlder(user: User, checkAge: number) {
  return checkAge > user.age;
}

Functions

Сама работа с функциями ничем не отличается с тем как мы можем с ними работать в чистом JavaScript. Но есть некоторые особенности при работе с типами в них.

Вернемся к нашей функции add

function add(n1: number, n2: number) {
  return n1 + n2;
}

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

function add(n1: number, n2: number): number {
  return n1 + n2;
}

TypeScript как при работе с переменными, так и с функциями следит за типами при работе кода. Поэтому если функция возвращает один тип, а переменная принимает другой, то он нам скажет об этом и покажет ошибку.

function add(n1: number, n2: number) {
  return n1 + n2;
}

function printResult(num: number) {
  console.log(num);
}

printResult(add(4, 6));

Тут ошибки не будет, т.к TypeScript просчитал какой тип вернет функция add и какой тип аргумента принимает printResult.

Но что за тип которые возвращает функция printResult? Может никакого из-за того, что нет ничего возвращенного? но нет! Есть специальный тип для этой ситуации и это - void. Он можно сказать как раз и обозначает, что наша функция не возвращает ничего.

Вы конечно скажите мне, что в таком случае в консоль к примеру нам вернут просто undefined! Да так и есть, вспоминая как работают функции в JavaScript, мы можем сказать, что если функция ничего не возвращает, то ее результат равен undefined. undefined является значением, который мы можем спокойно присваивать переменным. Но в ситуации с TypeSctipt есть маленькая техническая разница.

В TypeScript есть такой тип undefined и мы его можем указывать переменным и в данном случае, указать как результат выполнения функции

function add(n1: number, n2: number) {
  return n1 + n2;
}

function printResult(num: number): undefined {
  console.log(num);
}

printResult(add(4, 6));

но мы получим ошибку

 A function whose declared type is neither 'void' nor 'any' must return a value.

дело в том, что в TypeScript undefined можно указать как тип возвращаемого значения только в том случае, если мы написали return внутри нее

function add(n1: number, n2: number) {
  return n1 + n2;
}

function printResult(num: number): undefined {
  console.log(num);
  return;
}

printResult(add(4, 6));

void же будет работать в обоих этих случаях.

Function as type

Мы часто в работе пишем функции через Functional Expression. В таком случае переменная должна иметь какой-то тип описывающий ту функцию которую она хранит. Вот ситуация:

function add(n1: number, n2: number) {
  return n1 + n2;
}

function printResult(num: number): undefined {
  console.log(num);
  return;
}

let test;

test = add;
test = 5;
test = printResult;

В данном коде ошибки не будет. Почему? Дело в том, что переменная test при создании получила автоматически тип any. В этом случае мы можем в нее записывать все что угодно. Но нам это не подходит. Мы хотим чтобы переменная test принимала, только функцию наподобие add. Что ж есть такой тип как Function, давайте его применим.

function add(n1: number, n2: number) {
  return n1 + n2;
}

function printResult(num: number): undefined {
  console.log(num);
  return;
}

let test: Function;

test = add;
// test = 5;
test = printResult;

Теперь нам пришлось убрать присвоение числа 5 в переменную, уже успех! И получилось, что мы можем указать в переменную test только функции. Но как персонализировать нам этот тип, чтобы можно было указать, какие параметры функция может принимать и какой тип возвращает. Тут нам поможет специальный синтаксис описания типа функции. Он похож на нам известную стрелочную функцию

function add(n1: number, n2: number) {
  return n1 + n2;
}

function printResult(num: number): undefined {
  console.log(num);
  return;
}

let test: (a: number, b: number) => number;

test = add;
// test = 5;
// test = printResult;

Теперь все работает как нужно! Синтаксис тут прост, мы в круглых скобках описываем аргументы функции, именная аргументов не особо важны, т.к. мы ихх нигде не используем. Советую давать им персонализированные имена в том случае когда хотите иметь подсказку. Ну а возвращаемый тип прописывается после стрелочки =>.

Callbacks

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

Как описать тип и работать с колл-бек функцией? Посмотрим на примере:

function add(n1: number, n2: number, cb: (result: number) => void) {
  const amount = n1 + n2;
  cb(amount);
}

add(4, 6, (result) => { console.log(result) });

Мы уже знаем как описать тип функции и поэтому тут сложности нет. Нам достаточно описать функцию, которая будет передана как параметр и использовать нашу функцию add как обычно.

Один нюанс хоу отметить, что тут

add(4, 6, (result) => { console.log(result) });

Нам не нужно прописывать какой тип должен быть аргумент result. Это из-за того, что это мы уже сделали тут cb: (result: number) => void. TypeScript сам проследит цепочку и выдаст ошибку если что-то тут будет не так.

never

Очень специфичный тип обозначающий, что функция возвращает абсолютно ничего. Такая ситуация может произойти если внутри функции бесконечный цикл или используется throw.

function add(message: string, errorCode: number): never {
  throw { message, errorCode };
}

add('Error', 500);

Тут также можно использовать void, но при void мы также можем получить результат undefined. Что в данной ситуации не совсем точно. Для точности и чистоты тут подходит именно этот тип never.


Written by Vadim Goloviychuk