@@ -262,6 +262,29 @@ export function isObject(value: unknown): value is object {
262262 return value != null && typeof value === 'object' ;
263263}
264264
265+ export function isPlainObject (
266+ value : unknown
267+ ) : value is Record < PropertyKey , unknown > {
268+ if ( ! isObject ( value ) ) {
269+ return false ;
270+ }
271+
272+ const proto = Object . getPrototypeOf ( value ) as typeof Object . prototype | null ;
273+
274+ const hasObjectPrototype =
275+ proto === null ||
276+ proto === Object . prototype ||
277+ Object . getPrototypeOf ( proto ) === null ;
278+
279+ return hasObjectPrototype
280+ ? Object . prototype . toString . call ( value ) === '[object Object]'
281+ : false ;
282+ }
283+
284+ function isUnsafeProperty ( key : PropertyKey ) {
285+ return key === '__proto__' || key === 'constructor' || key === 'prototype' ;
286+ }
287+
265288export function isEventListenerObject ( x : unknown ) : x is EventListenerObject {
266289 return isObject ( x ) && 'handleEvent' in x ;
267290}
@@ -501,5 +524,60 @@ export function setStyles(
501524 element : HTMLElement ,
502525 styles : Partial < CSSStyleDeclaration >
503526) : void {
504- Object . assign ( element . style , styles ) ;
527+ merge ( element . style , styles ) ;
528+ }
529+
530+ /**
531+ * Merges the properties of `source` into `target` performing a recursive deep merge over POJOs and arrays.
532+ *
533+ * @remarks
534+ * This function mutates the `target` object.
535+ * If that is not the desired outcome, see {@link toMerged} for another approach.
536+ */
537+ export function merge <
538+ T extends Record < PropertyKey , any > ,
539+ S extends Record < PropertyKey , any > ,
540+ > ( target : T , source : S ) : T & S {
541+ const sourceKeys = Object . keys ( source ) as Array < keyof S > ;
542+ const length = sourceKeys . length ;
543+
544+ for ( let i = 0 ; i < length ; i ++ ) {
545+ const key = sourceKeys [ i ] ;
546+
547+ if ( isUnsafeProperty ( key ) ) {
548+ continue ;
549+ }
550+
551+ const sourceValue = source [ key ] ;
552+ const targetValue = target [ key ] ;
553+
554+ if ( Array . isArray ( sourceValue ) ) {
555+ if ( Array . isArray ( targetValue ) ) {
556+ target [ key ] = merge ( targetValue , sourceValue ) ;
557+ } else {
558+ target [ key ] = merge ( [ ] , sourceValue ) ;
559+ }
560+ } else if ( isPlainObject ( sourceValue ) ) {
561+ if ( isPlainObject ( targetValue ) ) {
562+ target [ key ] = merge ( targetValue , sourceValue ) ;
563+ } else {
564+ target [ key ] = merge ( { } , sourceValue ) ;
565+ }
566+ } else if ( targetValue === undefined || sourceValue !== undefined ) {
567+ target [ key ] = sourceValue ;
568+ }
569+ }
570+
571+ return target ;
572+ }
573+
574+ /**
575+ * Just like {@link merge} but it does not mutate the `target` object instead
576+ * mutating a structured clone of it.
577+ */
578+ export function toMerged <
579+ T extends Record < PropertyKey , any > ,
580+ S extends Record < PropertyKey , any > ,
581+ > ( target : T , source : S ) : T & S {
582+ return merge ( structuredClone ( target ) , source ) ;
505583}
0 commit comments