Обобщения
Одна из ключевых идей в разработке ПО - это создание многократно используемых модулей.
В таких языках как C# или Java для разработки многократно используемых модулей используется такой инструмент как 'generics', позволяющий создавать компоненты, которые могут работать со множеством типов, а не с одним. И это позволяет использовать в этих компонентах типы собственной разработки.
"Hello World" с помощью обобщений
Для начала создадим "hello world" с помощью обощенных типов: функцию идентификации, которая будет возвращать все, что в нее передано. Что то вроде команды
'echo'.
Без обобщенных типов необходимо явно указывать тип аргумента и тип возвращаемого значения:
function identity(arg: number): number {
return arg;
}
Но можно в качестве типа указать тип 'any':
function identity(arg: any): any {
return arg;
}
В этом случает в аргумент 'arg' может быть любого типа, как и результат, возвращаемый функцией. Но даже если аргумент будет типа
number, возвращаемый результат все равно может быть типа any. Таким образом теряется связь между типом аргумента и типом возвращаемого значения.
Но бывает необходимо связать тип аргумента и возвращаемого значения. Для этого нужно использовать переменную типа, это специальная переменная, которая работает как тип, а не как значение.
function identity<T>(arg: T): T {
return arg;
}
Здесь добавлена переменная типа 'T'. Она позволяет зафиксировать тип аргумента для последующего использования. Так же 'T' указывается как возвращаемый тип. Таким образом получается, что возвращается именно такой тип, который передан в функцию.
Теперь можно сказать, что данная функция обощенная, она работает со множеством типов, и тип аргемента связан с типом результата.
Вызвать такую функцию можно двумя способами. Первый - передать все аргументы, включая переменную типа:
var output = identity<string>("myString"); // результат будет типа 'string'
Здесь явно указыватся, что 'T' будет строка.
Второй способ, возможно, более частый - это не указывать 'T', при этом компилятор автоматически установит значение 'T' в зависимости от типа аргумента:
var output = identity("myString"); // результат будет типа 'string'
Однако, не редко, если код более сложный, бывает необходимость указывать тип 'T' явно.
Работа с переменными обобщений
Когда пользователь разрабатывает обощенные функции, компилятор следит чтобы обощенные параметры использовались корректно в теле функции.
Посмотрим на первоначальную функцию:
function identity<T>(arg: T): T {
return arg;
}
Добавим вывод длины аргументы в консоль. Можно попробовать сделать что то вроде:
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // Ошибка: T не имеет .length
return arg;
}
В этом случае компилятор выдаст ошибку, что используется поле ".length" аргумента 'arg', но нигде не указано, что 'arg' имеет это поле. Не все типы имеют это поле.
Перепишем функцию так, чтобы работать с массивом переменных типа T. Тогда у аргумента будет поле
.length:
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length); // Ошибки нет, т.к. у массива имеется поле .length
return arg;
}
Можно сказать, что обощенная функция loggingIdentity принимает параметр типа T и аргумент 'arg' в виде массива T, и возвращает массив 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;
}
var myIdentity: <T>(arg: T)=>T = identity;
Но можно указывать и другое имя обобщающего типа.
function identity<T>(arg: T): T {
return arg;
}
var myIdentity: <U>(arg: U)=>U = identity;
Или так:
function identity<T>(arg: T): T {
return arg;
}
var myIdentity: {<T>(arg: T): T} = identity;
Напишем первый обощенный интерфейс. Изменим предыдущий пример с использованием интерфейса:
interface GenericIdentityFn {
<T>(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
var myIdentity: GenericIdentityFn = identity;
Обобщенный параметр можно вынести в интерфейс. В этом случае тип параметра будет виден всем членам интерфейса.
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
var myIdentity: GenericIdentityFn<number> = identity;
Также как и обобщенные интерфейсы можно создавать обобщенные классы, но нельзя создавать обобщенный перечисления и модули.
Обобщенные классы
Обобщенные классы выглядят так же как обобщенные интерфейсы.
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
var myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
Это очень похоже на класс 'GenericNumber'.
var stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };
alert(stringNumeric.add(stringNumeric.zeroValue, "test"));
Теперь можно гарантировать, что все свойства класса работают с одним и тем же типом.
Класс имеет две части: статическую и экземплярную. Обобщенные классы являются такими только в своей экземплярной части, но не в статической, поэтомк когда работаем с классами, их статические свойства не могут использовать обобщенный тип.
Ограничения обобщений
Если вы понимте предыдущий пример, иногда нужно написать обобщенную функцию с несколькими параметрами, где известны некоторые особенности этих типов параметров. В примере 'loggingIdentity', мы хотим получать доступ к ".length" свойству переменной 'arg', но компилятор не знает, что каждый тип имеет свойство ".length", поэтому он сообщает об ошибке.
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // Ошибка: T не имеет .length
return arg;
}
Вместо того, чтобы работать с любыми типами, мы можем указать, что функция работат с типами, которые имеют свойство ".length".
Для этого надо создать интерфейс, который будет описывать наше ограничение. Создадим интерфейс с одним свойством ".length", а затем используем его:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Теперь мы знаем, что свойство .length имеется
return arg;
}
Но теперь функция не будет работать с абсолютно всеми типами:
loggingIdentity(3); // Ошибка, number не имеет свойство .length
Вместо этого необходимо передавать параметры с явным указанием свойств:
loggingIdentity({length: 10, value: 3});
Использование параметров типов в ограничениях обобщений
Иногда бывает указать параметр типа, который ограничивается другим параметром типа. Например,
function find<T, U extends Findable<T>>(n: T, s: U) { // ошибка
// ...
}
find (giraffe, myAnimals);
Решить эту задачу можно заменив параметр типа ограничением:
function find<T>(n: T, s: Findable<T>) {
// ...
}
find(giraffe, myAnimals);
Использование типов классов в обобщениях
Когда используются фабрики в TypeScript с применением обощений, необходимо ссылаться на тип класса по его конструктору. Например,
function create<T>(c: {new(): T; }): T {
return new c();
}
Более продвинутый пример использует свойство prototype и ограничивает отношения между функцией конструктора и экземплярной частью класса.
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 extends Animal, K> (a: {new(): A;
prototype: {keeper: K}}): K {
return a.prototype.keeper;
}
findKeeper(Lion).nametag; // typechecks!