Spec-Zone.ru › TypeScript 5.1

Обобщения

Эта страница руководства заменена, перейдите на новую страницу

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

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

Привет, мир обобщений

Для начала давайте рассмотрим «привет, мир» обобщений: функцию идентичности. Функция идентичности — это функция, которая возвращает то, что ей передано. Вы можете представить это аналогично команде echo.

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

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

Или мы могли бы описать функцию идентичности с помощью типа any:

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

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

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

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

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

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

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

let output = identity<string>("myString");

Здесь мы явно задаём T как string в качестве одного из аргументов вызова функции, используя <> вокруг аргументов, а не ().

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

let output = identity("myString");

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

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

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

Давайте рассмотрим нашу функцию identity из предыдущего примера:

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

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

function loggingIdentity<T>(arg: T): T {
  console.log(arg.length);
  return arg;
}

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

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

function loggingIdentity<T>(arg: T[]): T[] {
  console.log(arg.length);
  return arg;
}

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

Мы можем альтернативно записать пример таким образом:

function loggingIdentity<T>(arg: Array<T>): Array<T> {
  console.log(arg.length); // Array has a .length, so no more error
  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), фактически фиксируя то, что будет использовать базовая сигнатура вызова. Понимание, когда следует помещать параметр типа непосредственно в сигнатуру вызова, а когда — в сам интерфейс, будет полезно при описании аспектов типа, которые являются обобщенными.

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

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

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

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}
 
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
  return x + y;
};

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

let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function (x, y) {
  return x + y;
};
 
console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));

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

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

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

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

function loggingIdentity<T>(arg: T): T {
  console.log(arg.length);
  return arg;
}

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

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

interface Lengthwise {
  length: number;
}
 
function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length); // Now we know it has a .length property, so no more error
  return arg;
}

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

loggingIdentity(3);

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

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

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

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

function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}
 
let x = { a: 1, b: 2, c: 3, d: 4 };
 
getProperty(x, "a");
getProperty(x, "m");

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

При создании фабрик в 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 createInstance<A extends Animal>(c: new () => A): A {
  return new c();
}
 
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;

© 2012-2023 Microsoft
Licensed under the Apache License, Version 2.0.
https://www.typescriptlang.org/docs/handbook/generics.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API