Урок 11

Практика: типобезопасные утилиты

Собрали функции, где дженерики дают реальную пользу.

pick

function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
    const result = {} as Pick<T, K>;
    keys.forEach((key) => {
        result[key] = obj[key];
    });
    return result;
}

const user = { name: 'Алиса', age: 25, email: 'a@b.com' };
const preview = pick(user, ['name', 'email']);
// { name: string; email: string }

omit

function omit<T extends object, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {
    const result = { ...obj };
    keys.forEach((key) => delete result[key]);
    return result;
}

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[]>);
}

sortBy

function sortBy<T, K extends keyof T>(items: T[], key: K, direction: 'asc' | 'desc' = 'asc'): T[] {
    return [...items].sort((a, b) => {
        const av = a[key];
        const bv = b[key];
        if (av < bv) return direction === 'asc' ? -1 : 1;
        if (av > bv) return direction === 'asc' ? 1 : -1;
        return 0;
    });
}

uniqueBy

function uniqueBy<T, K extends keyof T>(items: T[], key: K): T[] {
    const seen = new Set<T[K]>();
    return items.filter((item) => {
        if (seen.has(item[key])) return false;
        seen.add(item[key]);
        return true;
    });
}

indexBy

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

mapValues

function mapValues<T extends object, R>(
    obj: T,
    fn: (value: T[keyof T]) => R
): Record<keyof T, R> {
    const result = {} as Record<keyof T, R>;
    (Object.keys(obj) as (keyof T)[]).forEach((key) => {
        result[key] = fn(obj[key]);
    });
    return result;
}

chunk

function chunk<T>(items: T[], size: number): T[][] {
    const result: T[][] = [];
    for (let i = 0; i < items.length; i += size) {
        result.push(items.slice(i, i + size));
    }
    return result;
}

invoke

Типобезопасный вызов метода:

function invoke<T, K extends keyof T>(
    obj: T,
    key: K
): T[K] extends (...args: any[]) => infer R ? R : never {
    const fn = obj[key];
    return typeof fn === 'function' ? fn.call(obj) : (undefined as never);
}

Создание API-клиента

type ApiRoutes = {
    '/users': { get: () => User[]; post: (body: UserInput) => User };
    '/users/:id': { get: (id: number) => User };
};

function createClient<R extends Record<string, Record<string, (...args: any[]) => any>>>() {
    return function call<K extends keyof R, M extends keyof R[K]>(
        route: K,
        method: M,
        ...args: Parameters<R[K][M]>
    ): ReturnType<R[K][M]> {
        // ...
    };
}

Итоги

  • pick, omit, groupBy, sortBy
  • uniqueBy, indexBy, mapValues, chunk
  • Дженерики сохраняют точные типы
  • keyof защищает от опечаток
  • Такие утилиты удобно выносить в lib/