Урок 03

Ограничения (extends)

extends ограничивает возможные типы параметра. Без него T — что угодно. С ним — только подходящее.

Базовый пример

function logLength<T extends { length: number }>(value: T): T {
    console.log(value.length);
    return value;
}

logLength('hello');    // ок: string имеет length
logLength([1, 2, 3]);  // ок: массив имеет length
logLength(42);         // ошибка: number не имеет length

Ограничение на примитив

function toString<T extends string | number>(value: T): string {
    return String(value);
}

Ограничение на объект

function getKeys<T extends object>(obj: T): (keyof T)[] {
    return Object.keys(obj) as (keyof T)[];
}

object — любой объект, но не примитив.

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

interface HasId {
    id: number;
}

function findById<T extends HasId>(items: T[], id: number): T | undefined {
    return items.find((item) => item.id === id);
}

const users = [{ id: 1, name: 'Алиса' }];
findById(users, 1);

T обязан иметь id: number.

keyof

Ограничение по ключам объекта:

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

const user = { name: 'Алиса', age: 25 };
const name = getValue(user, 'name'); // string
const age = getValue(user, 'age');   // number
getValue(user, 'email'); // ошибка
  • K extends keyof T — K является ключом T
  • T[K] — тип значения по ключу

Ограничение на union ключей

type UserKeys = 'name' | 'email';

function getUserField<T extends { [K in UserKeys]: string }>(obj: T, key: UserKeys): string {
    return obj[key];
}

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

function merge<T extends object, U extends object>(a: T, b: U): T & U {
    return { ...a, ...b };
}

Ограничение с дефолтом

type Container<T extends object = {}> = {
    items: T;
};

Ограничение на класс

class Animal {
    name = '';
}

function getName<T extends Animal>(animal: T): string {
    return animal.name;
}

T — наследник Animal.

Самоссылка

type Comparable<T> = {
    compareTo(other: T): number;
};

function max<T extends Comparable<T>>(a: T, b: T): T {
    return a.compareTo(b) > 0 ? a : b;
}

Практика: pluck

function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
    return items.map((item) => item[key]);
}

const users = [
    { name: 'Алиса', age: 25 },
    { name: 'Боб', age: 30 },
];

pluck(users, 'name'); // string[]
pluck(users, 'age');  // number[]

Практика: groupBy

function groupBy<T, K extends keyof T>(items: T[], key: K): Record<string, T[]> {
    return items.reduce((acc, item) => {
        const group = String(item[key]);
        acc[group] = acc[group] ?? [];
        acc[group].push(item);
        return acc;
    }, {} as Record<string, T[]>);
}

Итоги

  • extends ограничивает параметр
  • keyof — для ключей
  • T[K] — тип значения по ключу
  • Несколько ограничений — через пересечение
  • Ограничения делают код безопаснее