Learning Book

Что такое дженерики и зачем они нужны

Что такое дженерики

Проблема без дженериков

Представьте функцию, которая возвращает первый элемент массива:

// Без дженериков — теряем информацию о типе
function first(arr: any[]): any {
  return arr[0]
}

const num = first([1, 2, 3])   // тип: any — плохо!
const str = first(['a', 'b'])  // тип: any — плохо!

TypeScript не знает, какой тип вернётся. Мы теряем все преимущества типизации.

Решение: параметры типов

// С дженериком — тип сохраняется
function first<T>(arr: T[]): T | undefined {
  return arr[0]
}

const num = first([1, 2, 3])   // тип: number | undefined
const str = first(['a', 'b'])  // тип: string | undefined

<T> — это параметр типа. Это как обычный параметр функции, но для типов.

Буква T — просто соглашение (от «Type»). Можно использовать любое имя: Item, Element, Value. Несколько параметров: <T, U>, <K, V>.

Встроенные дженерики

Вы уже используете дженерики каждый день:

// Array<T> — массив элементов типа T
const numbers: Array<number> = [1, 2, 3]
const strings: string[] = ['a', 'b'] // сокращённая запись Array<string>

// Promise<T> — промис, который разрешается значением типа T
const fetchUser = (): Promise<User> => {
  return fetch('/api/user').then(r => r.json())
}

// Map<K, V> — словарь с ключами типа K и значениями типа V
const cache = new Map<string, number>()
cache.set('age', 25)

// Set<T> — множество элементов типа T
const ids = new Set<number>([1, 2, 3])

Вывод типов (Type Inference)

TypeScript часто может вывести параметр типа автоматически:

function identity<T>(value: T): T {
  return value
}

// TypeScript сам определяет T = number
const result1 = identity(42)        // T выведен как number
const result2 = identity('hello')   // T выведен как string

// Явное указание типа (обычно не нужно)
const result3 = identity<boolean>(true)

TypeScript использует унификацию для вывода параметров типов. Компилятор смотрит на переданные аргументы и «решает» уравнение, подбирая тип.

Для identity(42) компилятор видит: параметр value имеет тип T, передаётся значение 42 типа number, значит T = number.

При неоднозначности TypeScript пробует найти наилучший общий тип:

function merge<T>(a: T, b: T): T[] {
  return [a, b]
}

// T выводится как string | number — общий тип
const mixed = merge(1, 'hello') // T = string | number

Несколько параметров типов

// Функция с двумя параметрами типов
function pair<A, B>(first: A, second: B): [A, B] {
  return [first, second]
}

const p1 = pair(1, 'hello')      // [number, string]
const p2 = pair(true, [1, 2])    // [boolean, number[]]

// Generic словарь
function zip<K extends string | number | symbol, V>(
  keys: K[],
  values: V[]
): Record<K, V> {
  const result = {} as Record<K, V>
  keys.forEach((key, i) => {
    result[key] = values[i]
  })
  return result
}

const obj = zip(['a', 'b'], [1, 2]) // { a: number, b: number }