Как в typescript объединить ключи, и если появляются повторы, то сделать объединение типов?

Отличный вопрос! В TypeScript для объединения ключей объектов с обработкой повторяющихся ключей (через объединение типов) существует несколько мощных подходов. Рассмотрим их подробно.

1. Базовый подход с keyof и пересечением типов

Самый простой способ — использовать пересечение типов (&):

type CombineObjects<T, U> = T & U;

// Пример использования
type User = {
  id: number;
  name: string;
  email: string;
};

type UserProfile = {
  id: string; // Конфликтующий ключ - разные типы
  age: number;
  avatar: string;
};

type Combined = CombineObjects<User, UserProfile>;
// Результат: { id: number & string, name: string, email: string, age: number, avatar: string }

Проблема: При конфликте типов создается пересечение (number & string), которое может стать never.

2. Продвинутое решение с объединением типов для конфликтующих ключей

Вот более практичное решение, где конфликтующие ключи объединяются через |:

type Merge<T, U> = {
  [K in keyof T | keyof U]: 
    K extends keyof U 
      ? K extends keyof T 
        ? T[K] | U[K] // Если ключ есть в обоих - объединяем типы
        : U[K] // Если ключ только в U
      : K extends keyof T 
        ? T[K] // Если ключ только в T
        : never;
};

// Пример использования
type User = {
  id: number;
  name: string;
  email: string;
  preferences: string[];
};

type UserProfile = {
  id: string; // Конфликт: number | string
  age: number;
  preferences: { theme: string }; // Конфликт: string[] | { theme: string }
};

type MergedUser = Merge<User, UserProfile>;
/* Результат:
{
  id: number | string;
  name: string;
  email: string;
  age: number;
  preferences: string[] | { theme: string };
}
*/

3. Рекурсивное слияние для вложенных объектов

Для глубокого слияния вложенных объектов:

type DeepMerge<T, U> = {
  [K in keyof T | keyof U]: 
    K extends keyof U 
      ? K extends keyof T 
        ? T[K] extends object
          ? U[K] extends object
            ? DeepMerge<T[K], U[K]> // Рекурсивное слияние объектов
            : T[K] | U[K]
          : T[K] | U[K]
        : U[K]
      : K extends keyof T 
        ? T[K] 
        : never;
};

// Пример
type Config = {
  database: {
    host: string;
    port: number;
  };
  cache: {
    enabled: boolean;
  };
};

type OverrideConfig = {
  database: {
    port: string; // Конфликт: number | string
    timeout: number;
  };
};

type MergedConfig = DeepMerge<Config, OverrideConfig>;
/* Результат:
{
  database: {
    host: string;
    port: number | string;
    timeout: number;
  };
  cache: {
    enabled: boolean;
  };
}
*/

4. Утилитарные типы для различных сценариев

Приоритет второму объекту

type PreferSecond<T, U> = {
  [K in keyof T | keyof U]: 
    K extends keyof U ? U[K] : 
    K extends keyof T ? T[K] : never;
};

Слияние с кастомной логикой разрешения конфликтов

type MergeWithResolver<T, U, Resolver> = {
  [K in keyof T | keyof U]: 
    K extends keyof U 
      ? K extends keyof T 
        ? K extends keyof Resolver
          ? Resolver[K] // Используем кастомный резолвер
          : T[K] | U[K] // По умолчанию - объединение
        : U[K]
      : K extends keyof T ? T[K] : never;
};

// Пример с резолвером
type ConflictResolver = {
  id: string; // Всегда использовать string для id
  preferences: { theme: string; language: string }; // Кастомный тип
};

type ResolvedMerge = MergeWithResolver<User, UserProfile, ConflictResolver>;

5. Практическая реализация с runtime-функцией

// Тип
type MergeTypes<T, U> = {
  [K in keyof T | keyof U]: 
    K extends keyof U 
      ? K extends keyof T 
        ? T[K] | U[K]
        : U[K]
      : K extends keyof T ? T[K] : never;
};

// Runtime функция
function mergeObjects<T extends object, U extends object>(
  obj1: T, 
  obj2: U
): MergeTypes<T, U> {
  return { ...obj1, ...obj2 } as MergeTypes<T, U>;
}

// Использование
const user = { id: 1, name: "John", email: "john@example.com" };
const profile = { id: "user_1", age: 30, avatar: "avatar.jpg" };

const merged = mergeObjects(user, profile);
// Тип: { id: number | string, name: string, email: string, age: number, avatar: string }

6. Обработка нескольких объектов

type MergeAll<T extends any[]> = T extends [infer First, ...infer Rest]
  ? Rest extends []
    ? First
    : MergeTypes<First, MergeAll<Rest>>
  : never;

function mergeAllObjects<T extends object[]>(...objects: T): MergeAll<T> {
  return Object.assign({}, ...objects) as MergeAll<T>;
}

// Пример
const obj1 = { a: 1, b: "hello" };
const obj2 = { a: "test", c: true };
const obj3 = { b: 42, d: [1, 2, 3] };

const result = mergeAllObjects(obj1, obj2, obj3);
// Тип: { a: number | string, b: string | number, c: boolean, d: number[] }

Ключевые моменты:

  1. keyof T | keyof U — получает объединение всех ключей
  2. Условные типы — определяют, присутствует ли ключ в каждом объекте
  3. Объединение типов (|) — решает конфликты
  4. Рекурсия — для глубокого слияния вложенных структур

Эти подходы позволяют гибко работать с объединением объектов в TypeScript, обеспечивая типобезопасность и автоматическое разрешение конфликтов типов.