Table of Contents #

Введение #

Важной частью разработки является построение компонентов, которые обладали бы не только строгим и последовательным API, но и были доступны для повторного использования. Наиболее гибкие возможности для построения крупной программной системы дают компоненты, способные работать и с теми данными, которые есть сегодня, и с теми, которые могут появиться завтра.

В таких языках, как C# и Java, одним из главных инструментов для создания компонентов, предназначенных для повторного использования, являются обобщения (также известные как шаблоны или дженерики). Они позволяют создавать компоненты, способные работать с различными типами, а не только с каким-то одним. Это позволяет применять такие компоненты и использовать свои типы.

Здравствуй, мир обобщений! #

Для начала создадим традиционную для знакомства c обобщениями первую функцию: функцию-тождество. Такая функция возвращает в точности то, что ей было передано. Можно расценивать ее так же, как команду echo.

Без использования обобщений пришлось бы задать такой функции определенный тип:

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

Или же описать ее, используя тип any:

function identity(arg: any): any {
    return arg;
}

Хотя использование any, без сомнения, представляет собой некоторого рода обобщение, поскольку позволяет использовать arg любого типа, в этом случае в момент возврата значения теряется информация о его типе. Если бы мы передали число, известно было бы лишь то, что это мог быть любой (any) тип.

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

function identity(arg: T): T {
    return arg;
}




Мы добавили типовую переменную T к функции-тождеству. Эта T позволяет сохранять тип, который указал пользователь (то есть number), так что позже его можно будет использовать. В данном случае T используется в качестве типа возвращаемого значения. Можно увидеть, что теперь и аргумент, и возвращаемое значение имеют один и тот же тип. Такой способ позволяет направить информацию о типах со входа функции к ее выходу.

Можно сказать, что этот вариант функции identity является обобщенным, посколько он работает со многими типами. В отличие от варианта с использованием any, он также является точным в том смысле, что не теряет информации о типах, так же как и самый первый вариант, где для аргумента и для возвращаемого значения использовался тип number.

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

let output = identity("myString");  // у output будет тип string

В этом примере T явно устанавливается в string, как один из аргументов функции, но окруженный угловыми скобками <> вместо круглых ().

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

let output = identity("myString");  // у output будет тип string

Обратите внимание, что тип не передается явно, в угловых скобках (<>) — компилятор просто проанализировал значение "myString" и установил T в значение его типа. Хотя выведение типового аргумента может быть полезно, чтобы сделать код более кратким и читаемым, иногда может понадобиться явно передавать типовый аргумент, если компилятору не удается автоматически вывести тип, что может произойти в более сложных случаях.

Работа с обобщенными типовыми переменными #

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

Возьмем уже знакомую нам функцию identity:

function identity(arg: T): T {
    return arg;
}

Что, если нужно при каждом вызове функции выводить длину аргумента arg в консоль? Может появиться искушение написать так:

function loggingIdentity(arg: T): T {
    console.log(arg.length);  // Ошибка: у T нет свойства .length
    return arg;
}

Если сделать подобное, компилятор выдаст ошибку, говорящую о том, что используется .length объекта arg, хотя нигде не было указано, что у объекта есть такое свойство. Ранее говорилось о том, что типовая переменная означает абсолютно любой тип, поэтому в функцию могло быть передано и число, у которого нет свойства .length.

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

function loggingIdentity(arg: T[]): T[] {
    console.log(arg.length);  // У массива есть .length, поэтому ошибки больше нет
    return arg;
}

Тип loggingIdentity читается как "обобщенная функция loggingIdentity, которая принимает типовый параметр T и аргумент arg, который является массивом объектов T, и возвращает массив объектов T". Если функции будет передан массив чисел, то результатом также будет массив чисел, так как T станет number. Это позволяет использовать обобщенную типовую переменную T как часть типа, с которым мы работаем, а не только как целый тип, что дает большую гибкость.

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

function loggingIdentity(arg: Array): Array {
    console.log(arg.length);  // У массива есть .length, поэтому ошибки больше нет
    return arg;
}

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

Обобщенные типы #

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

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

function identity(arg: T): T {
    return arg;
}

let myIdentity: (arg: T) => T = identity;

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

function identity(arg: T): T {
    return arg;
}

let myIdentity: <U>(arg: U) => U = identity;

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

function identity(arg: T): T {
    return arg;
}

let myIdentity: {(arg: T): T} = identity;

Это подводит нас к описанию первого обобщенного интерфейса. Возьмем объектный литерал из предыдущего примера и превратим его в интерфейс:

interface GenericIdentityFn {
    (arg: T): T;
}

function identity(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn = identity;

Возможно, нам захотелось бы сделать обобщенный параметр параметром интерфейса в целом. Такой подход позволит понимать, относительно какого типа (или типов) происходит обобщение (то есть относительно Dictionary<string>, а не просто Dictionary). Это делает типовый параметр доступным всем остальным членам интерфейса.

interface GenericIdentityFn {
    (arg: T): T;
}

function identity(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn = identity;

Обратите внимание, что пример трансформировался в нечто совершенно иное. Вместо описания обобщенной функции теперь обычная, не обобщенная функция, которая является частью обобщенного типа. При использовании GenericIdentityFn теперь придется указывать соответствующий типовый аргумент (в данном случае number), таким образом зафиксировав типы, которые будет использовать соответствующая функция. Понимать, в каких случаях типовый параметр нужно добавлять к сигнатуре вызова, а когда — к самому интерфейсу, полезно при описании того, какие аспекты типа являются обобщенными.

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

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

Обобщенные классы имеют такой же вид, что и обобщенные интерфейсы. У них есть список типовых параметров в угловых скобках (<>) после имени класса.

class GenericNumber {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

Это довольно буквальное использование типа GenericNumber (букв. обобщенное число), но можно заметить, что ничего не мешает использовать с ним и другие типы, кроме number. К примеру, можно использовать тип string или более сложные объекты.

let stringNumeric = new GenericNumber();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };

alert(stringNumeric.add(stringNumeric.zeroValue, "test"));

Так же, как и с интерфейсами, передача типового параметра самому классу устанавливает то, что все его свойства будет работать с одним и тем же типом.

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

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

Как вы помните из предыдущих примеров, в некоторых случаях требуется создать обобщенную функцию, которая работает с некоторым множеством типов, в отношении которых известно, какие возможности у них есть. В примере с loggingIdentity требовалось получить доступ к свойству .length объекта arg, но компилятор не мог быть уверен, что у любого типа будет такое свойство, поэтому предупреждал об этом.

function loggingIdentity(arg: T): T {
    console.log(arg.length);  // Ошибка: у T нет свойства .length
    return arg;
}

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

Чтобы реализовать это, создадим интерфейс, который описывал бы такое ограничение. Создадим интерфейс с единственным свойством .length, и используем его с ключевым словом extend, чтобы обозначить ограничение:

interface Lengthwise {
    length: number;
}

function loggingIdentity(arg: T): T {
    console.log(arg.length);  // Теперь мы знаем, что у объекта есть свойство .length, поэтому ошибки нет
    return arg;
}

Поскольку обобщенная функция теперь имеет ограничение, она не сможет работать с любым типом:

loggingIdentity(3);  // Ошибка, у числа нет свойства .length

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

loggingIdentity({length: 10, value: 3});

Использование типовых параметров в ограничениях обобщений

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

function copyFields(target: T, source: U): T {
    for (let id in source) {
        target[id] = source[id];
    }
    return target;
}

let x = { a: 1, b: 2, c: 3, d: 4 };

copyFields(x, { b: 10, d: 20 }); // все в порядке
copyFields(x, { Q: 90 });  // ошибка: у 'x' нет свойства 'Q'

Использование типов классов в обобщениях

Реализуя паттерн "фабрика" с использованием обобщений, необходимо указывать на тип класса с помощью его функции-конструктора. К примеру,

function create(c: {new(): T; }): T {
    return new c();
}

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

class BeeKeeper {
    hasMask: boolean;
}

class ZooKeeper {
    nametag: string;
}

class Animal {
    numLegs: number;
}

class Bee extends Animal {
    keeper: BeeKeeper;
}

class Lion extends Animal {
    keeper: ZooKeeper;
}

function findKeeper (a: {new(): A;
    prototype: {keeper: K}}): K {

    return a.prototype.keeper;
}

findKeeper(Lion).nametag;  // проверка типов!

Источник







Поддержите перевод документации:



Поддерживатель | Github Репозиторий


Documentation generated by mdoc.
Молния! Обновления, новости и статьи Typescript.