Repository: sindresorhus/type-fest (TypeScript)

Issue:
`SharedUnionFieldsDeep` with `undefined` in Union
`SharedUnionFieldsDeep` doesn't work with Union that have `undefined`.

For example :
```ts
type Cat = {
  type: 'cat';
  name: string;
  catType: string;
};

type Dog = {
  type: 'dog';
  name: string;
  dogType: string;
};

type Pet = SharedUnionFieldsDeep<Cat | Dog>;
// type Pet = {
//   type: "cat" | "dog";
//   name: string;
// }

type OptionalPet = SharedUnionFieldsDeep<Cat | Dog | undefined>;
// type OptionalPet = Cat | Dog | undefined
```

Here, `OptionalPet` keeps the full union, but we might expect the following output :

```ts
type OptionalPet = SharedUnionFieldsDeep<Cat | Dog | undefined>;
// type Pet = {
//   type: "cat" | "dog";
//   name: string;
// } | undefined
```


Discussion:
I think it's reasonable to handle invalid types yourself before passing them to a function. I'd think `Parameters<SomeFunc | undefined>` also fails the same way for example. 
I don't think this is a problem, it makes sense to merge public fields only if the original types is consistent, like if they are both an object.


I see your point. I thought that it might be a good addition with an option like `recurseIntoArrays` (maybe `allowUndefinedInUnion` ?).
Especially with complex and deep types :
```ts
type A = {
  a: number;
  b: string;
  c: { d: string; e: number } | { d: string; f: string } | undefined;
}
```
With this example, it would be quite powerful to have something like this :
```ts
type SharedUnionA = SharedUnionFieldsDeep<A, {allowUndefinedInUnion: true}>;
// type SharedUnionA = {
//   a: number;
//   b: string;
//   c: { d: string } | undefined;
// }
```
Which is correct since as soon as `c` is defined, then `d` is `string`.



Patch:
diff --git a/source/shared-union-fields-deep.d.ts b/source/shared-union-fields-deep.d.ts
index cf5fb198d..00ed0b36a 100644
--- a/source/shared-union-fields-deep.d.ts
+++ b/source/shared-union-fields-deep.d.ts
@@ -98,13 +98,18 @@ export type SharedUnionFieldsDeep<Union, Options extends SharedUnionFieldsDeepOp
 Same as `SharedUnionFieldsDeep`, but accepts only `object`s and as inputs. Internal helper for `SharedUnionFieldsDeep`.
 */
 type SharedObjectUnionFieldsDeep<Union, Options extends SharedUnionFieldsDeepOptions> =
+	// `keyof Union` can extract the same key in union type, if there is no same key, return never.
 	keyof Union extends infer Keys
 		? IsNever<Keys> extends false
 			? {
 				[Key in keyof Union]:
 				Union[Key] extends NonRecursiveType
 					? Union[Key]
-					: SharedUnionFieldsDeep<Union[Key], Options>
+					// Remove `undefined` from the union to support optional
+					// fields, then recover `undefined` if union was already undefined.
+					: SharedUnionFieldsDeep<Exclude<Union[Key], undefined>, Options> | (
+						undefined extends Required<Union>[Key] ? undefined : never
+					)
 			}
 			: {}
 		: Union;


Test patch:
diff --git a/test-d/shared-union-fields-deep.ts b/test-d/shared-union-fields-deep.ts
index 2c8140ea5..ed0b0a958 100644
--- a/test-d/shared-union-fields-deep.ts
+++ b/test-d/shared-union-fields-deep.ts
@@ -132,3 +132,14 @@ expectType<TestingType>(same);
 // Test for propagation with non union root object
 declare const nonUnionRootType: SharedUnionFieldsDeep<{union: {number: number; boolean: boolean} | {number: number}}>;
 expectType<{union: {number: number}}>(nonUnionRootType);
+
+declare const unionWithUndefined: SharedUnionFieldsDeep<{
+	a?: {a: string; foo: number} | {a: string; bar: string} | undefined;
+	b?: {a: string; foo: number} | {a: string; bar: string};
+	c: {a: string; foo: number} | {a: string; bar: string} | undefined;
+}>;
+expectType<{
+	a?: {a: string} | undefined;
+	b?: {a: string};
+	c: {a: string} | undefined;
+}>(unionWithUndefined);