Last updated
Last updated
const mergedProps = mergeProps(
{ onClick: fn1 },
{ onClick: fn2, className: "blue" },
{ onClick: fn3, className: "button", style: { display: "block" }},
{ style: { display: "flex", color: "red" }}
);
/**
{
className: "blue button",
style: { display: "flex", color: "red" },
onClick: () => {
fn1();
fn2();
fn3();
}
}
*/
function pushProp(target: Record<string, any>, key: string, value: any): void {
if (key === "className") {
target.className = [target.className, value]
.filter(Boolean)
.join(" ")
.trim();
} else if (key === "style") {
target.style = { ...target.style, ...value };
} else if (typeof value === "function") {
target[key] = target[key] ? chain(target[key], value) : value;
} else if (
// skip merging undefined values
value === undefined ||
// skip if both value are the same primitive value
(typeof value !== "object" && value === target[key])
) {
return;
} else if (!Object.prototype.hasOwnProperty.call(target, key)) {
target[key] = value;
} else {
throw new Error(
`Didn’t know how to merge prop '${key}'. ` +
`Only 'className', 'style', and event handlers are supported`
);
}
}
/**
* Merges sets of props together:
* - duplicate `className` props get concatenated
* - duplicate `style` props get shallow merged (later props have precedence for conflicting rules)
* - duplicate functions (to be used for event handlers) get called in order from left to right
* @param props Sets of props to merge together. Later props have precedence.
*/
export function mergeProps<T extends Record<string, any>[]>(
...props: T
): {
[K in keyof UnionToIntersection<T[number]>]: K extends "className"
? string
: K extends "style"
? UnionToIntersection<T[number]>[K]
: Exclude<Extract<T[number], { [Q in K]: unknown }>[K], undefined>;
} {
if (props.length === 1) {
return props[0] as any;
}
return props.reduce((merged, ps: any) => {
for (const key of Object.keys(ps)) {
pushProp(merged, key, ps[key]);
}
return merged;
}, {}) as any;
}