Универсальные типы - это мощный инструмент TypeScript, который часто используют неправильно из-за того, что они настолько отличаются от того, что разработчик, пришедший из традиционного JavaScript, использует.
В TypeScript универсальные типы позволяют нам определять тип заполнителя для переменной, которую мы еще не знаем достаточно, чтобы уточнить ее тип. Рассмотрим следующий фрагмент:
const sayMessage = (message: string): string => `Simon says ${message}`;
Предыдущая функция будет работать достаточно хорошо, если мы уверены, что аргумент send при вызове sayMessage всегда будет строкой. Однако могут возникнуть ситуации, которые могут предотвратить это. Например, аргумент сообщение исходит от третьей стороны, например, службы, базы данных, поля ввода и т. Д. Другая причина, по которой это может вызвать проблемы в дальнейшем, это возможность изменения дизайна приложения, и вдруг оно должно принимать числа.
Возьмем последнее условие. Предположим, мы получили запрос на изменение, позволяющий функции sayMessage получать не только строки, но и числа. Может быть, наша первая догадка - сделать что-то вроде этого:
// extending the parameter type scope with a Union type const sayMessage = (message: string | number): string => `Simon says ${message}`;
И это сработает. В конце концов, для этого и нужны типы союза. Однако я бы сказал, что это плохой архитектурный шаблон и его следует использовать экономно и только в пунктуальных случаях; поскольку такой подход в результате оставляет нашу простую функцию непригодной для масштабирования. Предположим, поступил новый запрос с просьбой разрешить принятие значений null в качестве параметров:
// not the best approach const sayMessage = (message: string | number | null): string => `Simon says ${message}`;
У вас даже может возникнуть соблазн попробовать это:
// not the best approach const sayMessage = (message: any): string => `Simon says ${message}`;
Что ж ... в этот момент, держу пари, вы начинаете чувствовать себя немного не в своей первоначальной догадке. Есть лучшее решение.
Реализация переменных типа как параметров функции
Более элегантный способ решить вышеупомянутую проблему - реализовать Переменные типа (также известные как «Общие»). Рассмотрим следующий фрагмент:
// leveraging a simple Generic Type // as a function parameter (lambda) const sayMessage = <T extends unknown>(message: T): string => `Simon says ${message}`;
Итак, здесь происходит ряд вещей. Во-первых, вы заметите ‹T extends unknown› прямо перед списком параметров функции. T на самом деле представляет собой общий тип. T - это то, что традиционно используется для этих универсальных типов (T означает тип), однако вы можете использовать все, что захотите, для имени универсального типа. Для лямбда функций необходимо добавить бит «extends unknown», иначе вы получите синтаксическую ошибку. Например, не-лямбда-функции могут определять ту же сигнатуру, которая была описана ранее, как показано ниже:
// leveraging a simple Generic Type // as a function parameter (non-lambda) function sayMessage<T>(message: T):string { return `Simon says ${message}`; }
Просто другой способ выразить то же чувство. Лично я предпочитаю лямбда-функции, но для каждой свои!
Следующее, что мы могли бы отметить, это то, что наш параметр message не является строкой, числом или строкой | номер, а тип T. Это позволяет нам делать следующее:
sayMessage('hello'); // returns 'Simon says hello' sayMessage(1); // returns 'Simon says 1' sayMessage(null); // returns 'Simon says null'
Все предыдущие примеры действительны, поскольку sayMessage теперь принимает аргументы универсального типа.
Одна интересная особенность использования универсальных типов заключается в том, что тип параметра также может использоваться в теле функции и даже возвращаться:
// returning the generic type // in the return expression of the function const sayMessage = <T extends unknown>(message: T): [ string, T ] => [ `Simon says ${message}`, message, ]; sayMessage('hello'); // returns [ 'Simon says hello', 'hello' ]
Предыдущий код вернет кортеж, состоящий из составленной строки (Саймон говорит «переменная T») и фактического параметра ‹T›, переданного как второй аргумент.
Ограничение типов в универсальном
Наш предыдущий пример универсального параметра может быть слишком широким и позволит потребителю функции вызывать его, отправляя такие типы, как undefined, null, или даже объект. Давайте предотвратим это, используя Общие ограничения.
Давайте ограничим, сколько пользователь функции может отправить:
// Generic Constraints via extending keyof another type or variable enum messages { greeting = 'hello', farewell = 'bye', love = 'I love you', } const sayMessage = <M, T extends keyof M>(messages: M, message: T): [ string, T ] => [ `Simon says ${messages[message]}`, message, ]; // returns [ 'Simon says bye', 'farewell' ] sayMessage(messages, 'farewell');
Специальный синтаксис «T extends keyof M» сообщает нам, что параметр T является членом первого параметра - типа M -. По сути, в нашем примере мы ограничиваем возможные варианты этого параметра тремя вариантами: «приветствие», «прощай» или «любовь '.
Использование интерфейса для ограничения универсального
Еще один изящный способ использования универсального типа - реализация интерфейсов, который также является еще одним способом ограничения области действия вашего типа. Следующий пример на самом деле является примером 2 в 1:
// Generic Constraints via interface extension interface IMessage<M> { message: M; emotion: string; } const sayMessage = <T extends IMessage<string>>(message: T): [ string, T ] => [ `Simon says ${message.message}${message.emotion}`, message, ]; // returns [ 'Simon says welcome!', { message: 'welcome', emotion: '!' } ] console.log(sayMessage({ message: 'welcome', emotion: '!' }));
Сначала мы создаем интерфейс IMessage. Этот интерфейс, в свою очередь, реализует универсальный тип (M). Интерфейс довольно прост: объект создается с двумя членами: сообщение (это общий тип M) и эмоция, которая является нить.
Затем наша функция sayMessage определяет, что параметр, который она получает (сообщение), является универсальным типом, расширяющим интерфейс IMessage. Это гарантирует, что этот параметр будет соответствовать установленному там контракту (требуется наличие сообщения и эмоции).
Обратите внимание на специальное обозначение этого универсального типа: ‹T extends IMessage ‹string››, это гарантирует, что член message интерфейса IMessage будет строка.
Типы утилит TypeScript
В TypeScript вы действительно можете увидеть, как универсальные типы используются во многих местах, на самом деле, для них даже есть специальный раздел в их документации.
Например, один из предпочтительных способов определения массива в TypeScript использует Generics:
let containers: Array<HTMLTableElement> = [];
Это определение, применяющее все знания, полученные из этой статьи, говорит нам, что переменная контейнеры будет использовать HTMLTableElement, который был определен как Generic Type в интерфейсе Array для TypeScript, который имеет следующую сигнатуру:
// Array<T> definition on TypeScripts source code interface Array<T> { ... }
Другой встроенный универсальный тип, который вы можете часто использовать, - это NonNullable. Этот тип определяется как пользовательский тип в исходном коде TypeScript:
type NonNullable<T> = T extends null | undefined ? never : T;
Довольно интересное определение. Здесь мы видим тернарное условие, в котором, если все, что происходит от типа ‹T›, является расширением null или undefined, оно вернет тип never («типы никогда заслуживают отдельной отдельной главы), по сути, делает« невозможным »быть либо null, либо undefined, в противном случае будет возвращен переданный тип T.
Я рекомендую вам изучить встроенные в TypeScript типы утилит, поскольку они могут сэкономить вам архитектурный дизайн, а также время разработки.
Заключение
Обобщения поддерживаются многими хорошо зарекомендовавшими себя языками программирования, такими как C # или Java, также имеет смысл реализовать их только в TypeScript.
Один из лучших вариантов использования Generics в TypeScript - предоставить типобезопасную переменную для неизвестных, таких как получение наборов данных API или принятие пользовательского ввода . Однако это не единственный вариант использования:
- Обеспечьте лучшую читаемость вашего кода
- Помогите вам выявить неожиданные сценарии с неизвестными переменными.
- Пусть ваша IDE даст вам лучшие предложения IntelliSense
- Помогите TypeScript правильно использовать данные, не являющиеся типобезопасными (ответы API, ввод данных пользователем, вычисления, синтаксический анализ файла JSON и т. Д.)
- Не допускайте присвоения типа "любой неизвестным переменным"
- Правильный ввод переменной также служит цели псевдодокументации, в то время как вы читаете код и понимаете, для чего используются переменные.
Еще темы, которые вы можете изучить:
Дальнейшее чтение: