Отличный вопрос! В 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[] }
Ключевые моменты:
keyof T | keyof U
— получает объединение всех ключей- Условные типы — определяют, присутствует ли ключ в каждом объекте
- Объединение типов (
|
) — решает конфликты - Рекурсия — для глубокого слияния вложенных структур
Эти подходы позволяют гибко работать с объединением объектов в TypeScript, обеспечивая типобезопасность и автоматическое разрешение конфликтов типов.