TLDR: Волхвы c! Попробуйте Playground
Итак, это сложный вопрос. Не столько из-за требований к слиянию, сколько из-за крайних случаев. Получение низко висящих фруктов заняло <20 минут. Чтобы убедиться, что это работает везде, понадобилось еще пару часов ... и утроил длину. Союзы сложно! </p>
Что такое необязательное свойство? В { a: 1 | undefined, b?: 1 }
является a
необязательным свойством? Некоторые люди говорят да. Другие нет. Лично я включаю в дополнительный список только b
.
Как вы справляетесь с профсоюзами? Каков вывод Merge<{}, { a: 1} | { b: 2 }>
? Я думаю, что тип, который имеет больше всего смысла, это { a?: 1 } | { b?: 2 }
. А как насчет Merge<string, { a: 1 }>
? Если вас не волнует профсоюзы, это легко ... если вы это делаете, тогда вы должны рассмотреть все это. (Что я выбрал в паренах)
Merge<never, never>
(never
) Merge<never, { a: 1 }>
({ a?: 1 }
) Merge<string, { a: 1 }>
(string | { a?: 1 }
) Merge<string | { a: 1 }, { a: 2 }>
(string | { a: 1 | 2 }
)
Давайте разберемся с этим типом, начиная с помощников.
У меня было намек, как только я подумал о профсоюзах, что этот тип станет сложным. У TypeScript нет хорошего встроенного способа проверки равенства типов, но мы можем написать вспомогательный тип, который вызывает ошибку компилятора, если два типа не равны.
(Примечание. Тип Test
может быть улучшено, это может позволить передавать типы, которые не эквивалентны, но этого достаточно для нашего использования здесь, оставаясь при этом довольно простым)
type Pass = 'pass';
type Test<T, U> = [T] extends [U]
? [U] extends [T]
? Pass
: { actual: T; expected: U }
: { actual: T; expected: U };
function typeAssert<T extends Pass>() {}
Мы можем использовать этот помощник следующим образом:
// try changing Partial to Required
typeAssert<Test<Partial<{ a: 1 }>, { a?: 1 }>>();
Далее нам понадобятся два типа помощников. Один, чтобы получить все необходимые ключи объекта, и один, чтобы получить дополнительные ключи. Во-первых, несколько тестов, чтобы описать, что мы ищем:
typeAssert<Test<RequiredKeys<never>, never>>();
typeAssert<Test<RequiredKeys<{}>, never>>();
typeAssert<Test<RequiredKeys<{ a: 1; b: 1 | undefined }>, 'a' | 'b'>>();
typeAssert<Test<OptionalKeys<never>, never>>();
typeAssert<Test<OptionalKeys<{}>, never>>();
typeAssert<Test<OptionalKeys<{ a?: 1; b: 1, c: undefined }>, 'a'>>();
Здесь нужно отметить две вещи. Во-первых, *Keys<never>
- это never
. Это важно, потому что мы будем использовать эти помощники в объединениях позже, и если объект never
, он не должен предоставлять никаких ключей. Во-вторых, ни один из этих тестов не включает проверки объединения. Учитывая, как я сказал, что союзы были, это может вас удивить. Однако эти типы используются только после того, как все союзы распределены, поэтому их поведение там не имеет значения (хотя, если вы включите их в свой проект, возможно, вы захотите взглянуть на указанное поведение, оно отличается от того, что вы, вероятно, ожидаете от RequiredKeys
из-за того, как написано)
Эти типы проходят указанные проверки:
type OptionalKeys<T> = {
[K in keyof T]-?: T extends Record<K, T[K]> ? never : K;
}[keyof T;
type RequiredKeys<T> = {
[K in keyof T]-?: T extends Record<K, T[K]> ? K : never;
}[keyof T] & keyof T;
Пара замечаний по этому поводу:
- Использование
-?
для удалите необязательные свойства, это позволяет нам избежать использования оболочки Exclude<..., undefined>
T extends Record<K, T[K]>
, поскольку { a?: 1 }
делает не расширяет { a: 1 | undefined }
. Я прошел несколько итераций, прежде чем окончательно остановился на этом. Вы также можете обнаружить опциональность с другим сопоставленным типом, как jcalz здесь . - В версии 3.8.3 TypeScript может правильно сделать вывод, что тип возвращаемого значения
OptionalKeys
назначается на keyof T
, Однако он не может обнаружить то же самое для RequiredKeys
. Пересечение с keyof T
исправляет это.
Теперь, когда у нас есть эти помощники, мы можем определить еще два типа, которые представляют вашу бизнес-логику c. Нам нужны RequiredMergeKeys<T, U>
и OptionalMergeKeys<T, U>
.
type RequiredMergeKeys<T, U> = RequiredKeys<T> & RequiredKeys<U>;
type OptionalMergeKeys<T, U> =
| OptionalKeys<T>
| OptionalKeys<U>
| Exclude<RequiredKeys<T>, RequiredKeys<U>>
| Exclude<RequiredKeys<U>, RequiredKeys<T>>;
И некоторые тесты, чтобы убедиться, что они ведут себя так, как ожидалось:
typeAssert<Test<OptionalMergeKeys<never, {}>, never>>();
typeAssert<Test<OptionalMergeKeys<never, { a: 1 }>, 'a'>>();
typeAssert<Test<OptionalMergeKeys<never, { a?: 1 }>, 'a'>>();
typeAssert<Test<OptionalMergeKeys<{}, {}>, never>>();
typeAssert<Test<OptionalMergeKeys<{ a: 1 }, { b: 2 }>, 'a' | 'b'>>();
typeAssert<Test<OptionalMergeKeys<{}, { a?: 1 }>, 'a'>>();
typeAssert<Test<RequiredMergeKeys<never, never>, never>>();
typeAssert<Test<RequiredMergeKeys<never, {}>, never>>();
typeAssert<Test<RequiredMergeKeys<never, { a: 1 }>, never>>();
typeAssert<Test<RequiredMergeKeys<{ a: 0 }, { a: 1 }>, 'a'>>();
Теперь, когда они у нас есть, мы можем определить слияние двух объектов, игнорируя примитивы и союзы на данный момент. Это вызывает тип верхнего уровня Merge
, который мы еще не определили для обработки примитивов и объединений членов.
type MergeNonUnionObjects<T, U> = {
[K in RequiredMergeKeys<T, U>]: Merge<T[K], U[K]>;
} & {
[K in OptionalMergeKeys<T, U>]?: K extends keyof T
? K extends keyof U
? Merge<Exclude<T[K], undefined>, Exclude<U[K], undefined>>
: T[K]
: K extends keyof U
? U[K]
: never;
};
(я не писал здесь специфических c тестов, потому что они у меня были на следующий уровень вверх)
Нам нужно обрабатывать как союзы, так и не-объекты. Давайте обрабатывать объединения объектов дальше. Как уже говорилось ранее, нам нужно распределить по всем типам и объединить их по отдельности. Это довольно просто.
type MergeObjects<T, U> = [T] extends [never]
? U extends any
? MergeNonUnionObjects<T, U>
: never
: [U] extends [never]
? T extends any
? MergeNonUnionObjects<T, U>
: never
: T extends any
? U extends any
? MergeNonUnionObjects<T, U>
: never
: never;
Обратите внимание, что у нас есть дополнительные проверки для [T] extends [never]
и [U] extends [never]
. Это связано с тем, что never
в распределительном предложении похож на for (let i = 0; i < 0; i++)
, он никогда не войдет в "тело" условного выражения и поэтому вернет never
, но нам нужно только never
, если обоих типы never
.
Мы почти у цели! Теперь мы можем обрабатывать объединяющиеся объекты, что является самой сложной частью этой проблемы. Осталось только обработать примитивы, что мы можем сделать, просто сформировав объединение всех возможных примитивов и исключив примитивы из типов, передаваемых в MergeObjects
.
type Primitive = string | number | boolean | bigint | symbol | null | undefined;
type Merge<T, U> =
| Extract<T | U, Primitive>
| MergeObjects<Exclude<T, Primitive>, Exclude<U, Primitive>>;
И с этим типом мы сделанный! Merge
ведет себя как угодно, всего около 50 строк безкомментированного безумия.
Последнее замечание о произведенных типах:
Результирующий тип из Merge
сейчас верен, но это не так читабельно, как могло бы быть. Прямо сейчас при наведении курсора на результирующий тип будет отображаться пересечение и внутренние объекты с обернутыми вокруг них Merge
вместо отображения результата. Мы можем исправить это, введя тип Expand
, который заставляет TS расширять все в один объект.
type Expand<T> = T extends Primitive ? T : { [K in keyof T]: T[K] };
Теперь просто измените MergeNonUnionObjects
для вызова Expand
. Где это необходимо, это несколько проб и ошибок. Вы можете поиграть с включением или нет, чтобы получить отображение типа, которое работает для вас.
type MergeNonUnionObjects<T, U> = Expand<
{
[K in RequiredMergeKeys<T, U>]: Expand<Merge<T[K], U[K]>>;
} & {
[K in OptionalMergeKeys<T, U>]?: K extends keyof T
? K extends keyof U
? Expand<Merge<
Exclude<T[K], undefined>,
Exclude<U[K], undefined>
>>
: T[K]
: K extends keyof U
? U[K]
: never;
}
>;
Проверьте это в на детской площадке , которая включает в себя все тесты, которые я использовал для проверить результаты.