Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add MergeDeep type #408

Closed
wants to merge 27 commits into from
Closed

Conversation

skarab42
Copy link
Collaborator

@skarab42 skarab42 commented Jun 27, 2022

Merge two types recursively into a new type.

Properties set to undefined value are skipped when strict option is set to true (default).
Array and plain object properties are merged recursively.

type Foo = {foo: string; bar: {id: string; label: string}};
type Bar = {foo: number; bar: {id: number; nop: undefined}};

type FooBar = MergeDeep<Foo, Bar>;

// {
// 	foo: number;
// 	bar: {
// 		id: number;
// 		label: string;
// 	}
// }

type FooBarLazy = MergeDeep<Foo, Bar, {strict:false}>;

// {
// 	foo: number;
// 	bar: {
// 		id: number;
// 		label: string;
//		nop: undefined;
// 	}
// }

@ilchenkoArtem @sindresorhus SetProp is comming soooon!

@skarab42 skarab42 marked this pull request as draft June 27, 2022 13:40
@skarab42 skarab42 marked this pull request as ready for review June 27, 2022 13:52
@skarab42 skarab42 mentioned this pull request Jun 27, 2022
@skarab42 skarab42 changed the title Add MergeDeep type Add MergeDeep type Jun 27, 2022
@sindresorhus
Copy link
Owner

Make sure you look at our existing "deep" types and that this new types handles all the cases handled by them.

For example,

type HasMultipleCallSignatures<T extends (...arguments: any[]) => unknown> =

readme.md Outdated Show resolved Hide resolved
source/merge-deep.d.ts Outdated Show resolved Hide resolved
source/merge-deep.d.ts Outdated Show resolved Hide resolved
source/merge-deep.d.ts Outdated Show resolved Hide resolved
source/merge-deep.d.ts Outdated Show resolved Hide resolved
source/merge-deep.d.ts Outdated Show resolved Hide resolved
@skarab42
Copy link
Collaborator Author

Make sure you look at our existing "deep" types and that this new types handles all the cases handled by them.

For example,

type HasMultipleCallSignatures<T extends (...arguments: any[]) => unknown> =

Sorry, I'm not sure what I'm supposed to test. Can you give me a concrete example?

source/internal.d.ts Outdated Show resolved Hide resolved
source/merge-deep.d.ts Outdated Show resolved Hide resolved

/**
`Unwrap` is like `Simplify` it flatten the type output to improve type hints shown in editors.
Unfortunately Simplify does not support the case where the type cannot be flattened.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this handle types that cannot be flattened differently than Simplify?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you can't iterate in the type then it can't be flattened, a function for example. Here is the simplest case TS Playground

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then I suggest we just improve the Simplify type.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect, I will make a PR ;)

index.d.ts Outdated Show resolved Hide resolved
source/merge-deep.d.ts Outdated Show resolved Hide resolved
source/merge-deep.d.ts Outdated Show resolved Hide resolved
@sindresorhus
Copy link
Owner

Sorry, I'm not sure what I'm supposed to test. Can you give me a concrete example?

I gave you a concrete example: HasMultipleCallSignatures. Relevant commit and tests: db54028

@sindresorhus
Copy link
Owner

Make sure you look at our existing "deep" types and that this new types handles all the cases handled by them.

Some other examples:

  • Should it recurse into arrays?
  • Should it recurse into Sets/Maps?

@skarab42
Copy link
Collaborator Author

  • Should it recurse into arrays?
  • Should it recurse into Sets/Maps?

At first I just wanted to contribute to a project I 💜 and improve my skills in the process by adding the SetProp type. While prototyping, I realized that I would need a Replace type to normalize the dotted paths. Then in several discussions I saw the need for the DeepMerge type, which would also simplify the creation of the SetProp type. Now, what I would love is to be able to type lodash.merge, but this is not necessarily what you need in type-fest.

In any case I think I have to start all over again and define before going any further the needs for this type.

If we want to go in the direction of lodash here is a non-exhaustive list cases I have been able to define:

// If the first type (the destination) is [number, string, boolean] return the destination.
_.merge(42, 24); // 42 
_.merge("42", "24"); // "42" 
_.merge(true, false); // true 

// [undefined, null, symbol, Set, Map] are always treated as empty objects regardless of location.
_.merge(undefined, null); // {} 
_.merge(null, undefined); // {} 
_.merge(Symbol(42), Symbol(24)); // {}
_.merge(new Set([42]), new Set([24])); // {} 
_.merge(new Map([["life", 42]]), new Map([["life", 42]])); // {}  

// If the first type is [Class, Function] return undefined.
_.merge(() => 42, {life:42}); // undefined 
_.merge(Math.floor, {life:42}); // undefined 
_.merge(Buffer, {life:42}); // undefined 

// If the first type is an object and the second is an array, the array is treated as an indexed object.
_.merge({"id":42}, [42]); // {"0":42,"id":42} 
_.merge(null, [42]); // {"0":42} // <- remeber null is treated as empty object

// If the first type is an array and the second is an object, the object is treated as an numerical indexed array stripping any string indexed properties.
_.merge([42], {"0":24,"id":42}); // [24] 
_.merge([42], null); // [42] 

// Instance of class is treated as an plain object.
class Foo {
  constructor() {
    this.id = "foo";
    this.foo = true;
  }
}

class Bar {
  constructor() {
    this.id = "bar";
    this.bar = true;
  }
}

_.merge(new Foo(), new Bar()); // {"id":"bar","foo":true,"bar":true}

@skarab42
Copy link
Collaborator Author

skarab42 commented Jul 3, 2022

I would have expected it to just concatenate the arrays.

In fact I have thought wrongly, I should rather think in terms of types and not values.

MergeDeep<{items: [1, 2, 3, 4, 5, 6]}, {items: ['a', 'b', 'c']}>;
// -> {items: ['a', 'b', 'c']}
// -> {items: string[]}

MergeDeep<{items: [1, 2, 3, 4, 5, 6]}, {items: ['a', 'b', 'c']}, {recurseIntoArrays: true}>;
// -> {items: [1, 2, 3, 4, 5, 6, 'a', 'b', 'c']}
// -> {items: (string|number)[]}

Any suggestions?

@sindresorhus
Copy link
Owner

In fact I have thought wrongly, I should rather think in terms of types and not values.

I agree with your examples.


// -> {items: string[]}

👍

// -> {items: (string|number)[]}

👍

@Uzlopak
Copy link

Uzlopak commented Jul 3, 2022

This is really interesting. If you like you could provide some useful typings to @fastify/deepmerge :)

@skarab42
Copy link
Collaborator Author

skarab42 commented Jul 4, 2022

@sindresorhus If you don't see anything to change, for me it's all good, the refactoring is done ;)

source/merge-deep.d.ts Outdated Show resolved Hide resolved
Merge two types recursively into a new type.

By default arrays are not supported, you have to explicitly enable it with the `recurseIntoArrays` option if you want it.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would explicitly document that arrays are concatenated type-wise.

test-d/merge-deep.ts Outdated Show resolved Hide resolved
@sindresorhus
Copy link
Owner

@skarab42 Bump :)

@skarab42
Copy link
Collaborator Author

@sindresorhus I'm working on a better version (I hope), it will take a few more days.

//=> never

MergeDeep<[], [], {recurseIntoArrays: true}>;
//=> []
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just showing a couple of edge-cases. It needs to show a real-world example usage.

// Test MergeDeep inputs

declare const signature1: MergeDeep<{}, {}>; // {}
declare const signature2: MergeDeep<[], []>; // Never
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
declare const signature2: MergeDeep<[], []>; // Never
declare const signature2: MergeDeep<[], []>; //=> never

@skarab42 skarab42 marked this pull request as draft August 22, 2022 17:39
@skarab42 skarab42 mentioned this pull request Aug 25, 2022
@skarab42
Copy link
Collaborator Author

I am closing this PR due to the new proposal #443.

@skarab42 skarab42 closed this Aug 26, 2022
@skarab42 skarab42 deleted the feat-merge-deep branch September 19, 2022 05:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants