TypeScript - Ограничения генериков: Раскрытие силы гибких типов

Привет, будущие маги TypeScript! Сегодня мы отправимся в увлекательное путешествие в мир ограничений генериков. Не волнуйтесь, если вы новички в программировании – я буду вашим доброжелательным проводником, и мы рассмотрим эту тему шаг за шагом. К концу этого учебника вы будете ограничивать генерики как профи!

TypeScript - Generic Constraints

Что такое ограничения генериков?

Прежде чем мы углубимся в детали, давайте начнем с простого аналога. Представьте, что у вас есть магическая коробка, которая может удерживать любой тип предметов. Это essentially и есть генерик в TypeScript – гибкий контейнер для разных типов. А что, если мы хотим поставить некоторые правила на то, что может идти в эту коробку? Вот где появляются ограничения генериков!

Ограничения генериков позволяют нам ограничить типы, которые могут использоваться с нашими генериками. Это как putting a label on our magical box saying, "Only objects with a 'length' property allowed!"

Примеры проблем: Why Do We Need Generic Constraints?

Давайте рассмотрим несколько сценариев, где ограничения генериков могут спасти положение:

Пример 1: Тайна свойства Length

function getLength<T>(item: T): number {
return item.length; // Error: Property 'length' does not exist on type 'T'
}

Ой! TypeScript выдает ошибку. Почему? Потому что не все типы имеют Свойство length. Что, если мы передадим в эту функцию число? Числа не имеют длины!

Пример 2: Загадочное сравнение

function compareValues<T>(value1: T, value2: T): boolean {
return value1 > value2; // Error: Operator '>' cannot be applied to types 'T' and 'T'
}

Другая ошибка! TypeScript не знает, можно ли сравнивать T с помощью >. Что, если мы передадим строки? Или сложные объекты?

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

Как работают ограничения генериков в TypeScript

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

Магическое слово extends

Чтобы добавить ограничение, мы используем слово extends. Это как decir TypeScript, "Hey, this type must have at least these properties or capabilities!"

Давайте исправим нашу функцию getLength:

interface Lengthwise {
length: number;
}

function getLength<T extends Lengthwise>(item: T): number {
return item.length; // No more error!
}

Давайте разберем это:

  1. Мы определяем интерфейс Lengthwise, который имеет Свойство length.
  2. Мы используем <T extends Lengthwise>, чтобы сказать "T must have at least what Lengthwise has".
  3. Теперь TypeScript знает, что бы ни было T, оно definitely have a length property!

Давайте проверим это:

console.log(getLength("Hello")); // Работает! Строки имеют длину
console.log(getLength([1, 2, 3])); // Работает! Массивы имеют длину
console.log(getLength(123)); // Error! Числа не имеют длины

Не правда ли здорово? Мы успешно ограничили наш генерик!

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

Иногда мы хотим ограничить один параметр типа на основе другого. Это как сказать, "Эта коробка может удерживать только предметы, compatible с тем, что уже в ней."

Давайте рассмотрим пример:

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

Что здесь происходит?

  1. У нас есть два параметра типа: T и U.
  2. T extends U означает, что T must be at least everything that U is, but it can have more.
  3. Это позволяет нам копировать свойства из source в target, зная, что target будет иметь все свойства, которые есть у source.

Давайте посмотрим, как это работает:

interface Person {
name: string;
}

interface Employee extends Person {
employeeId: number;
}

let person: Person = { name: "Alice" };
let employee: Employee = { name: "Bob", employeeId: 123 };

copyProperties(employee, person); // Работает!
copyProperties(person, employee); // Error! Person doesn't have employeeId

Практическое применение и лучшие практики

Теперь, когда мы понимаем, как работают ограничения генериков, давайте рассмотрим некоторые реальные примеры и лучшие практики:

  1. Ограничение до типов объектов: Часто вы хотите убедиться, что вы работаете с объектами:
function cloneObject<T extends object>(obj: T): T {
return { ...obj };
}
  1. Ограничение до типов функций: Вы можете убедиться, что тип вызываем:
function invokeFunction<T extends Function>(func: T): void {
func();
}
  1. Ограничение до конкретных свойств: Убедитесь, что объекты имеют конкретные свойства:
function getFullName<T extends { firstName: string; lastName: string }>(obj: T): string {
return `${obj.firstName} ${obj.lastName}`;
}
  1. Множественные ограничения: Вы можете применить несколько ограничений, используя оператор &:
function processData<T extends number & { toFixed: Function }>(data: T): string {
return data.toFixed(2);
}

Вот таблица, резюмирующая эти методы:

Метод Описание Пример
Ограничение до типов объектов Убедитесь, что тип является объектом <T extends object>
Ограничение до типов функций Убедитесь, что тип вызываем <T extends Function>
Ограничение до конкретных свойств Убедитесь, что тип имеет конкретные свойства <T extends { prop: Type }>
Множественные ограничения Combines multiple constraints <T extends TypeA & TypeB>

Заключение: Принятие силы ограничений

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

Помните, ключ к maîtriser ограничения генериков – это практика. Попробуйте переделать часть вашего существующего кода, используя генерики и ограничения. Вы будете удивлены, насколько чище и более надежным становится ваш код!

Заканчивая, вот немного программного юмора для вас: Why did the TypeScript developer go broke? Because he used too many generic constraints and couldn't accept any type of payment! ?

Продолжайте программировать, продолжайте учиться и, что самое главное, получайте удовольствие от TypeScript!

Credits: Image by storyset