Отличный перевод с примерами, typescript handbook, на русский язык
Advanced Types
Для начала создадим традиционную для знакомства c обобщениями первую функцию: функцию-тождество. Такая функция возвращает в точности то, что ей было передано. Можно расценивать ее так же, как командуecho
.
Без использования обобщений пришлось бы задать такой функции определенный тип:
function identity(arg: number): number {
return arg;
}
Или же описать ее, используя типany
:
function identity(arg: any): any {
return arg;
}
Хотя использованиеany
, без сомнения, представляет собой некоторого рода обобщение, поскольку позволяет использоватьarg
любого типа, в этом случае в момент возврата значения теряется информация о его типе. Если бы мы передали число, известно было бы лишь то, что это мог быть любой (any
) тип.
Вместо этого нужен способ захватить тип аргумента так, чтобы его впоследствии можно было использовать для описания типа возвращаемого значения. Здесь мы используемти́повую переменную— особый вид переменной, которая оперирует типами, а не значениями.
function identity<t>(arg: T): T {
return arg;
}
Мы добавили типовую переменнуюT
к функции-тождеству. ЭтаT
позволяет сохранять тип, который указал пользователь (то естьnumber
), так что позже его можно будет использовать. В данном случаеT
используется в качестве типа возвращаемого значения. Можно увидеть, что теперь и аргумент, и возвращаемое значение имеют один и тот же тип. Такой способ позволяет направить информацию о типах со входа функции к ее выходу.
Можно сказать, что этот вариант функцииidentity
является обобщенным, посколько он работает со многими типами. В отличие от варианта с использованиемany
, он также является точным в том смысле, что не теряет информации о типах, так же как и самый первый вариант, где для аргумента и для возвращаемого значения использовался типnumber
.
Функция написана, и теперь ее можно вызвать одним из двух способов. Первый способ — передать все аргументы, в том числе и типовый аргумент:
let output = identity<string>("myString"); // у output будет тип string
В этом примереT
явно устанавливается вstring
, как один из аргументов функции, но окруженный угловыми скобками<>
вместо круглых()
.
Второй способ, вероятно, наиболее популярен. Здесь используетсявыведение типового аргумента, и компилятор автоматически устанавливаетT
на основании типа аргумента, который передается в функцию:
let output = identity("myString"); // у output будет тип string
Обратите внимание, что тип не передается явно, в угловых скобках (<>
) — компилятор просто проанализировал значение"myString"
и установилT
в значение его типа. Хотя выведение типового аргумента может быть полезно, чтобы сделать код более кратким и читаемым, иногда может понадобиться явно передавать типовый аргумент, если компилятору не удается автоматически вывести тип, что может произойти в более сложных случаях.
Работа с обобщенными типовыми переменными
Начав применять обобщения, можно заметить, что при создании обобщенных функций, таких, какidentity
, компилятор будет принуждать к корректному использованию типовых переменных в теле функции. То есть, необходимо расценивать каждый из параметров так, как если бы он мог оказаться абсолютно любым типом.
Возьмем уже знакомую нам функциюidentity
:
function identity<t>(arg: T): T {
return arg;
}
Что, если нужно при каждом вызове функции выводить длину аргументаarg
в консоль? Может появиться искушение написать так:
function loggingIdentity<t>(arg: T): T {
console.log(arg.length); // Ошибка: у T нет свойства .length
return arg;
}
Если сделать подобное, компилятор выдаст ошибку, говорящую о том, что используется.length
объектаarg
, хотя нигде не было указано, что у объекта есть такое свойство. Ранее говорилось о том, что типовая переменная означает абсолютно любой тип, поэтому в функцию могло быть передано и число, у которого нет свойства.length
.
Допустим, что на самом деле функция должна работать с массивами объектовT
, а не с самими объектамиT
напрямую. Так как она будет иметь дело с массивами, у них должно быть свойство.length
. Можно описать это так, словно мы создаем массив:
function loggingIdentity<t>(arg: T[]): T[] {
console.log(arg.length); // У массива есть .length, поэтому ошибки больше нет
return arg;
}
ТипloggingIdentity
читается как "обобщенная функцияloggingIdentity
, которая принимает типовый параметрT
и аргументarg
, который является массивом объектовT
, и возвращает массив объектовT
". Если функции будет передан массив чисел, то результатом также будет массив чисел, так какT
станетnumber
. Это позволяет использовать обобщенную типовую переменнуюT
как часть типа, с которым мы работаем, а не только как целый тип, что дает большую гибкость.
Как вариант, можно записать этот пример следующим способом:
function loggingIdentity<t>(arg: Array<t>): Array<t> {
console.log(arg.length); // У массива есть .length, поэтому ошибки больше нет
return arg;
}
Вы уже могли встречаться с таким видом записи типов в других языках. В следующем разделе мы обсудим, как создавать свои собственные типы наподобиеArray<T>
.
Обобщенные типы
В предыдущих разделах мы создали обобщенную функцию-тождество, которая работала с различными типами. В этом разделе разберем, как описать тип такой функции и то, как создавать обобщенные интерфейсы.
Тип обобщенной функции схож с типом обычной функции, где типовый параметр указан первым, так же, как и в ее определении:
function identity<t>(arg: T): T {
return arg;
}
let myIdentity: <t>(arg: T) => T = identity;
Для типового параметра можно было бы использовать другое имя, важно лишь, чтобы число типовых параметров и то, как они используются, согласовывалось.
function identity<t>(arg: T): T {
return arg;
}
let myIdentity: <U>(arg: U) => U = identity;
Также можно записать обобщенный тип как сигнатуру вызова на типе объектного литерала:
function identity<t>(arg: T): T {
return arg;
}
let myIdentity: {<t>(arg: T): T} = identity;
Это подводит нас к описанию первого обобщенного интерфейса. Возьмем объектный литерал из предыдущего примера и превратим его в интерфейс:
interface GenericIdentityFn {
<t>(arg: T): T;
}
function identity<t>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
Возможно, нам захотелось бы сделать обобщенный параметр параметром интерфейса в целом. Такой подход позволит понимать, относительно какого типа (или типов) происходит обобщение (то есть относительно Dictionary<string>
, а не простоDictionary
). Это делает типовый параметр доступным всем остальным членам интерфейса.
interface GenericIdentityFn<t> {
(arg: T): T;
}
function identity<t>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
Обратите внимание, что пример трансформировался в нечто совершенно иное. Вместо описания обобщенной функции теперь обычная, не обобщенная функция, которая является частью обобщенного типа. При использованииGenericIdentityFn
теперь придется указывать соответствующий типовый аргумент (в данном случаеnumber
), таким образом зафиксировав типы, которые будет использовать соответствующая функция. Понимать, в каких случаях типовый параметр нужно добавлять к сигнатуре вызова, а когда — к самому интерфейсу, полезно при описании того, какие аспекты типа являются обобщенными.
Кроме обобщенных интерфейсов можно создавать и обобщенные классы. Обратите внимание, что создавать обобщенные перечисления и пространства имен нельзя.
Если будет интересно смотрите больше по ссыке: http://typescript-lang.ru/docs/Generics.html