TypeScript. Generics (дженерики или обобщения)

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

Я знакомился с дженериками, когда изучал Java. Но с изучением их в TypeScript у меня возникли некоторые сложности. Сперва мне показалось, что они, по сути, являются лишними и тот же функционал можно реализовать, используя обычное указание типов. Так, например, в Java дженерики используются для следующего:

List <Person> persons;

Тут мы создаем список, который может хранить только объекты Person. Запихнуть в него что-то другое не выйдет. И, зная это, с элементами списка можно спокойно работать как с персонами (вызывать соответствующие методы). Но в TypeScript подобное мы можем сделать следующим образом:

let persons: Person[];

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

Начнем с простого примера - javascript функции "эхо", которая просто возвращает переданное в нее значение.

function echo (arg) {
    return arg;
}

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

function echo (arg:number): number {
    return arg;
}

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

echo('Ау'); // Ошибка: тип 'string' не подходит

Пишем другую функцию и описываем ее под работу со строками:

function echo2 (arg:string): string {
    return arg;
}
echo2('Ау');
echo2(111); // Ошибка: тип 'number' не подходит

Не очень изящное решение...

Хорошо, тогда можно же описать нашу функцию под тип any:

function echo (arg:any): any {
    return arg;
}
echo('Ау');
echo(111);

Требуемая задача выполнена. Наша функция теперь работает и с числами, и со строками. Но тут есть два нюанса. Первый - функция также может принимать и все остальные типы, кроме необходимых. Это не всегда допустимо. А второй нюанс заключается в том, что использовав описание any мы теряем способность TypeScript правильно проверять типы на этапе компиляции. Давайте создадим тестовую функцию, чтобы это увидеть:

function test(params: string) { };

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

test(echo('Ау')); 
test(echo(1));

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

Вот тут на помощь и приходят обобщения:

function echo <T>(arg:T): T {
    return arg;
}

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

echo <string>('Ау');
echo <number>(1);

В первой строке мы используем строковый тип. Во второй - числовой. Явно тип можно не указывать. TypeScript самостоятельно выведет тип исходя из типа переданного значения:

echo ('Ау');
echo (1);

Теперь воспользуемся нашей тестовой функцией, чтобы посмотреть, работает ли проверка типов:

test(echo('Ау'));
test(echo(1)); // Ошибка подсвечивается. Проверка типа работает

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

Тип для обобщенной функции

Описать тип обобщенной функции можно почти так же, как и обычной, только добавляется типовая переменная, как при инициализации:

function echo <T>(arg: T): T {
    return arg;
}

let myEcho: <T>(arg: T) => T = echo;

Названия типовых переменных (T) в объявлении и инициализации могут отличаться. Главное, чтобы между ними было согласование.

let myEcho: <T>(arg: T) => T = function <E>(arg: E): E {
    return arg;
}

То же самое можно записать, используя объектную форму записи:

let myEcho: { <T>(arg: T):T } = function <E>(arg: E): E {
    return arg;
}

А здесь мы уже подобрались к обобщенным интерфейсам.

Обобщенные интерфейсы

Перенесем нашу последнюю объектную запись в интерфейс:

interface IEcho {
    <T>(arg: T):T 
}

let myEcho: IEcho = function <E>(arg: E): E {
    return arg;
}

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

interface IEcho <T> {
    (arg: T):T 
}

let myEcho: IEcho <string> = function <E>(arg: E): E {
    return arg;
}

Обобщенные классы

Очень похоже на описание интерфейса:

class User <T> {
    _id: T;
    constructor(id: T) {
        this._id = id;            
    }
    get id(): T{
        return this._id;
    } 
}

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

let pavel = new User<number>(13);
let alex = new User<string>('alex');

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

pavel = new User<string>('13'); // Ошибка

Ограничения обобщений

Иногда нужно в качестве типового параметра принимать какие-то определенные типы. Можно задать ограничение для обобщения. Предположим, нам нужна функция, которая умела бы работать с объектами, способными летать и плавать. Создадим два интерфейса, которые описывают возможность объектов летать и плавать. А нашу требуемую функцию run обобщим по типу, наследующему эти интерфейсы <T extends Flyable & Swimmable>:

interface Flyable {
    fly (): void
} 

interface Swimmable {
    swim (): void
} 

function run <T extends Flyable & Swimmable>(arg: T) {
    arg.fly();
    arg.swim();
    arg.go(); // Ошибка: 'go' does not exist on type 'T'
}

Попробуем передать в нашу функцию объект, который ни летать, ни плавать не умеет:

run(pavel); // Ошибка: Property 'fly' is missing in type 'User<number>'.

А теперь передадим утку:

let duck = {
    fly() {},
    swim() {},
    go () {}
}

run(duck); // Все ОК

Павел Прудников

Постигающий дзен фулстэк вэб буддизма

Минск, Беларусь

Подписаться на Блог MEAN stack разработчика

Получайте свежие записи прямо на ваш почтовый ящик.

Или подпишитесь на RSS с Feedly!

Комментарии

comments powered by Disqus