Ограничения дженериков: extends

Ограничения generic-параметров через extends позволяют указать, что тип T должен быть совместим с определённым типом, давая доступ к свойствам этого типа внутри generic-функции или класса.

Зачем нужно

Без ограничений TypeScript трактует T как unknown — нельзя обратиться ни к каким свойствам. T extends SomeType даёт компилятору гарантию что T имеет нужные поля, позволяя типобезопасно их использовать.

Где используется

  • Функции, работающие с объектами определённой формы: T extends { id: string }
  • Generic с ключами: K extends keyof T
  • Ограничение на конструктор: T extends new (...args) => any
  • Conditional types: T extends string ? ... : ...

Основной контент

Базовое ограничение

// Без ограничения — нельзя обратиться к свойствам
function identity<T>(x: T): T {
  // x.length; // Error: Property 'length' does not exist on type 'T'
  return x;
}

// С ограничением — гарантировано наличие length
function logLength<T extends { length: number }>(x: T): T {
  console.log(x.length); // OK
  return x;
}

logLength("hello");         // OK — string имеет length
logLength([1, 2, 3]);       // OK — array имеет length
logLength({ length: 5 });   // OK
// logLength(42);           // Error — number не имеет length

K extends keyof T

Самое распространённое ограничение — безопасный доступ к полю объекта по ключу.

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "Alice", age: 30, email: "a@b.com" };

const name  = getProperty(user, "name");  // string
const age   = getProperty(user, "age");   // number
// getProperty(user, "role");             // Error — нет такого ключа

// Установить значение
function setProperty<T, K extends keyof T>(
  obj: T,
  key: K,
  value: T[K]
): void {
  obj[key] = value;
}

setProperty(user, "age", 31);     // OK
// setProperty(user, "age", "old"); // Error — age: number, не string

Ограничение на интерфейс

interface HasId {
  id: string;
}

interface HasTimestamps {
  createdAt: Date;
  updatedAt: Date;
}

// T должен иметь id
function findById<T extends HasId>(
  items: T,
  id: string
): T | undefined {
  return items.find(item => item.id === id);
}

// T должен иметь id И timestamps
function updateTimestamp<T extends HasId & HasTimestamps>(
  entity: T
): T {
  return { ...entity, updatedAt: new Date };
}

Ограничение на конструктор

// T должен быть классом (конструктором)
function createInstance<T>(
  Constructor: new (...args: any) => T
): T {
  return new Constructor();
}

class Service {}
const service = createInstance(Service); // Service

// Mixin-паттерн
type Constructor<T = {}> = new (...args: any) => T;

function Serializable<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    serialize: string {
      return JSON.stringify(this);
    }
  };
}

Условные ограничения

// extends в conditional types — другое использование
type IsArray<T> = T extends any ? true : false;

type A = IsArray<string>; // true
type B = IsArray<string>;   // false

// Ограничение для conditional type
type NonNullableKeys<T> = {
  [K in keyof T]: null extends T[K] ? never : K;
}[keyof T];

interface User {
  id: string;
  name: string;
  email: string | null;
}

type RequiredKeys = NonNullableKeys<User>; // "id" | "name"

Несколько ограничений

// Пересечение через &
function process<T extends HasId & { name: string }>(
  entity: T
): string {
  return `${entity.id}: ${entity.name}`;
}

// Несколько независимых параметров с ограничениями
function transform<T extends object, K extends keyof T>(
  obj: T,
  keys: K
): Pick<T, K> {
  const result = {} as Pick<T, K>;
  keys.forEach(k => (result[k] = obj[k]));
  return result;
}

Частые ошибки

  • T extends any — то же что T без ограничения; это не "принять любой тип".
  • Ограничить слишком жёсткоT extends User вместо T extends { id: string } — лишняя специализация.
  • Путать extends в generic и в conditional<T extends X> ограничивает generic; T extends X ? A : B — conditional type.
  • Не использовать K extends keyof T для типобезопасного доступа к полям — без него TypeScript не знает что ключ существует.

Связанные темы

Ресурсы