Привет! В новой части руководства будут рассмотрены такие важные понятия, как литералы и дженерики. Итак, приступим.

Предыдущие части: Часть 1 - Введение и примитивы Часть 2 - Ссылочные типы данных Часть 3 - Классы и интерфейсы

Литералы в TypeScript

Кроме основных типов string и number, мы можем использовать конкретные строки и числа в качестве типов:

// Объединенный тип с литералами
let favoriteColor = 'red' | 'blue' | 'green' | 'yellow';

favoriteColor = 'blue';
favoriteColor 'black'; // ОШИБКА: Тип "'black'" не может быть присвоен типу "'red' | 'blue' | 'green' | 'yellow'"

Дженерики

Generic-типы (дженерики) позволяют создавать компоненты, которые будут работать с несколькими типами, а не с каким-то определенным, что помогает сделать компонент более переиспользуемым.

Давайте взглянем на пример, чтобы понять, что это значит.

Функция addId принимает любой объект и возвращает объект со всеми полями, которые были у входного объекта, добавляя ему поле id со случайным значением от 0 до 1000. Говоря проще, эта функция добавляет ID к любому объекту.

сonst addId = (obj: object) => {
	let id = Math.floor(Math.random() * 1000);
  
    return { ...obj, id };
};

let person1 = addId({ name: 'Джон', age: 40 });

console.log(person1.id); // 271
console.log(person1.name): // ОШИБКА: Свойство 'name' не существует для типа '{ id: number; }'.

Как вы могли заметить, TypeScript выдает ошибку, когда мы пытаемся вывести свойство name . Это происходит из-за того, что, когда мы передаем объект в функцию addId, мы не указываем, какие свойства должен иметь этот объект - поэтому TypeScript не знает о них. Так что единственное свойство, которое он знает в этом объекте - это id.

Как же мы можем передать любой объект в addId таким образом, чтобы TypeScript знал, какие у него есть свойства? Мы можем использовать дженерики. Они обозначаются как <T>, где T - это параметр типа:

// <T> используется для примера, мы можем использовать любую букву, например <X> или <A>
const addId = <T>(obj:T) => {
	let id = Math.floor(Math.random() * 1000);
  
    return { ...obj, id };
};

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

Однако теперь у нас есть проблема: мы может передать в addId все что угодно, и TypeScript примет это, не выводя никаких ошибок:

let person1 = addId({ name: 'Джон', age: 40 });
let person2 = addId('Салли'); // Передаем строку и никакой ошибки

console.log(person1.id); // 271
console.log(person1.name): // Джон

console.log(person2.id); // 890
console.log(person2.name); // ОШИБКА: Свойство 'name' не существует для типа '"Салли" & { id: number; }'.

Когда мы передаем строку, TypeScript не видит проблемы. Он сообщает об ошибке только когда мы пытаемся обратиться к свойству name. Надо поставить ограничение: необходимо сказать TypeScript, что можно передавать только объекты. Для этого надо сделать наш дженерик T расширением object:

const addId = <T extends object>(obj:T) => {
	let id = Math.floor(Math.random() * 1000);

    return { ...obj, id };
};

let person1 = addId({ name: 'Джон', age: 40 });
let person2 = addId('Салли'); // ОШИБКА: Невозможно задать аргумент типа 'string' для параметра типа 'object'.

Теперь мы сразу видим ошибку - это хорошо, но не совсем то, что нам нужно. Массивы в JavaScript так же являются объектами, так что мы до сих пор можем передать в нашу функцию массив вместо объекта:

let person2 = addId(['Салли', 26]); // Передаем в функцию массив - никаких проблем

console.log(person2.id); // 890
console.log(person2.name); // ОШИБКА: Свойство 'name' не существует для типа '(string | number)[] & { id: number; }'.

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

const addId = <T extends { name: string }>(obj: T) => {
	let id = Math.floor(Math.random() * 1000);
  
    return { ...obj, id };
};
let person2 = addId(['Салли', 26]); // ОШИБКА: Аргумент должен содержать свойство 'name' со строковым значением.

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

// Ниже мы явно указываем между угловыми скобками, какого типа должен быть аргумент
let person1 = addId<{ name: string; age: number }>({ name: 'Джон', age: 40 });

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

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

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

function logLength(a: any) {
	console.log(a.length); // Все в порядке
	return a;
}

let hello = 'Hello world';
loglength(hello); // 11

let howMny = 8;
logLength(howMany); // undefined (TypeScript не показывает ошибку, а хотелось бы)

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

function logLength<T>(a: T) {
	console.log(a.length); // ОШИБКА: TypeScript не уверен, что 'a' имеет свойство 'length'
	return a;
}

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

Решение: использовать дженерик, который наследует интерфейс, требующий, чтобы у аргумента было свойство length:

interface hasLength {
	length: number;
}

function logLength<T extends hasLength>(a: T) {
    console.log(a.length); // Теперь ошибки нет
    return a;
}

let hello = 'Hello world';
loglength(hello); // 11

let howMny = 8;
logLength(howMany); // ОШИБКА: у числа нет свойства 'length'

Мы также можем написать функцию, в которой аргументом будет массив, в котором все элементы будут иметь свойство length:

interface hasLength {
	length: number;
}
function logLengths<T extends hasLength>(a: T[]) {
    a.forEach(element => {
        console.log(element.length);
    });
}

let arr = [
    'У этой строки есть длина',
    ['У массива', 'тоже есть', 'длина'],
    { material: 'plastic', length: 40 },
];

logLengths(arr);
// 24
// 3
// 40

Таким образом - дженерики очень крутая особенность TypeScript!

Дженерики с интерфейсами

Когда мы не знаем, какой конкретно тип у определенного объекта будет в будущем, мы можем использовать дженерик, чтобы передать этот тип:

// Тип T будет передан в интерфейс
interface Person<T> {
	name: string;
	age: number;
	documents: T;
}

// Мы должны передать тип поля 'documents', в данном случае - массив строк
const person1: Person<string[]> = {
    name: 'Павел',
    age: 21,
    documents: ['паспорт', 'полис', 'снилс'],
};

// Теперь мы снова добавляем интерфейс 'Person', на этот раз передаем ему тип 'string'
const person1: Person<string> = {
    name: 'Павел',
    age: 21,
    documents: 'паспорт, зачетка',
};

Перечисления в TypeScript

Перечисления (enums) - это еще одна интересная возможность TypeScript. Они позволяют нам определить или объявить коллекцию связанных значений (строк или чисел) в виде набора именованных констант.

enum ResourceType {
	BOOK,
	AUTHOR,
	FILM,
	DIRECTOR,
	PERSON,
}

console.log(ResourceType.BOOK); // 0
console.log(ResourceType.AUTHOR); // 1

// Начать с 1
enum ResourceType {
    BOOK = 1,
    AUTHOR,
    FILM,
    DIRECTOR,
    PERSON,
}

console.log(ResourceType.BOOK); // 1
console.log(ResourceType.AUTHOR); // 2

По умолчанию, перечисления основаны на числах - они хранят строковые значения, в виде чисел. Однако они так же могут быт строками:

enum Direction {
	Up = 'Up',
	Right = 'Right',
	Down = 'Down',
	Left = 'Left',
}

console.log(Direction.Right); // Right
console.log(Direction.Down); // Down

Перечисления полезны, когда у нас есть набор связанных констант. Например, вместо использования неинформативных чисел в вашем коде можно использовать более понятные константы.

Перечисления также помогают избежать багов, когда вы введете название перечисления, intellisense подскажет вам возможные опции, которые можно выбрать.

На этом четвертая часть руководства заканчивается, надеюсь, она вам будет полезна.

В следующей (и заключительной) части мы рассмотрим строгий режим в TypeScript и сужение типов. До встречи!

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