For a project I'm working on, I required my objects to be defined with at least one of the property from the object for example below is types that defines these options
type DraggableOptions = {
identifier?: {
id?: string;
type?: string;
};
modifiers?: {
dropEffect?: "copy" | "move";
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data?: any;
}
export type DroppableOptions = {
identifier: {
id?: string;
type?: string[];
};
modifiers?: {
highlight?: {
on: "dragmove" | "dragover";
class: string;
};
};
};
export type GlobalOptions = {
draggableOptions?: DraggableOptions;
droppableOptions?: DroppableOptions;
};
As all the properties are optional(because I also have default options) user can just pass a empty object {} when passing the DraggableOptions which I want to restrict to define at least one property from the object.
I also found a solution for this in the following stack overflow question but it does not work for nested objects which is my requirement. So based on the solution I have found I tried to write a recursive one also but could not get it right.
I found these two particular solution to be working in my case.
type AtLeastOne<T> = {
[K in keyof T]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<keyof T, K>>>;
}[keyof T];
type AtLeastOne<T, U = { [K in keyof Required<T>]: Pick<Required<T>, K> }> = Partial<T> &
U[keyof U];
And I tried the following solution for recursive but they are not working. I want to make not only the direct properties to follow this rule but also the nested objects.
type RecursiveAtLeastOne<T> = {
[K in keyof T]: T[K] extends object ? AtLeastOne<T[K]> : T[K];
};
and
type RecursiveAtLeastOne<T> = {
[K in keyof T]: T[K] extends Record<string, any> ? AtLeastOne<T[K]> : T[K];
};
Can any please guide me what I'm doing wrong and what would be the right solution for this problem? Thank you so much in advance.
Btw as I am not able to write the recursive version correctly I'm using the following approach to make nested objects at least on required
export type DraggableOptions = AtLeastOne<{
identifier?: AtLeastOne<{
id?: string;
type?: string;
}>;
modifiers?: AtLeastOne<{
dropEffect?: "copy" | "move";
}>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data?: any;
}>;
Below is the usage example
// This redrop class set's the global draggable and droppable options
// It accepts draggableOptions and droppableOptions but not required
// for the user to always define these options
const redrop = new Redrop({
draggableOptions: {
identifier: {
id: "id",
},
},
});
const draggable = document.querySelector("#item1");
// makeDraggable accepts 2nd parameter as draggableOptions
redrop.makeDraggable(draggable, {
identifier: {
id: "id",
},
});
// draggableOptions are optional and user can just do the following
redrop.makeDraggable(draggable);
// But as I have made all properties as optional(?) user can
// also do something like this -
const redrop = new Redrop({}) // empty object without any options
// or
const redrop = new Redrop({
draggableOptions: {} // no options defined
droppableOptions: {}
})
// or
redrop.makeDraggable(draggable, {});
After tinkering a bit I found a solution which might not be the best but as of now working as expected.
To recap, I had a few types in which every property is optional so it was possible for the user to just pass a {} object as all properties are optional user need to not to specify any property. To solve this I found a solution on the stack overflow itself which is following
type AtLeastOne<T> = {
[K in keyof T]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<keyof T, K>>>;
}[keyof T];
This allowed to create a constraint to provide at least one property when defining the object instead of just a "{}" empty object. It worked but it does not work on nested properties so if the object had nested object and I want to enforce the same rule, the current AtLeastOne type did not solve this issue.
To fix this issue I created the recursive type of AtLeastOne type which is following
type RecursiveAtLeastOne<T> = {
[K in keyof T]: T[K] extends object ? AtLeastOne<T[K]> : T[K];
};
Initially I thought this is not working because all my properties were optional
type DraggableOptions = {
identifier?: {
id?: string;
type?: string;
};
modifiers?: {
dropEffect?: "copy" | "move";
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data?: any;
}
But when I removed the (?) to make the properties required the RecursiveAtLeastOne started working as I wanted with just one catch. It only applied the rule to the nested objects for example "identifier" and "modifier" which are nested in the main DraggableOptions this meant I can not just define {identifier: {}}
or {modifier: {}}
I needed to provide at least one property but it was still possible to just define empty draggable option like this redrop.makeDraggable(draggable, {});
Finally to fix this I just wrapped the RecursiveAtLeastOne in another AtLeastOne like below
type RecursiveAtLeastOne<T> = AtLeastOne<{
[K in keyof T]: T[K] extends object ? AtLeastOne<T[K]> : T[K];
}>;
So to summarise to apply the at least one rule I need to make all the properties required by default so that I can use the AtLeastOne
and RecursiveAtLeastOne
type to make them optional but also making sure at least one property is always defined.