Skip to content

Commit 454badb

Browse files
author
Hector Arce De Las Heras
committed
Enhancing Type Safety in useStyles, useStylesV2, and mergeObjects Functions
This commit improves type safety in the useStyles, useStylesV2, and mergeObjects functions, along with their associated tests. The types of customTokens and the objects being merged are now more strictly defined. Changes include: useStyles and useStylesV2 functions now require T to extend object, and customTokens is now of type Partial<T>. This ensures that customTokens is a subset of T. mergeObjects function now requires its target and sources parameters to extend object. The return type is now Partial<T & S>, indicating that the result is a subset of the union of T and S. Tests for mergeObjects have been updated to reflect the new types. The objects being merged are now explicitly typed.
1 parent 98e9d3c commit 454badb

File tree

5 files changed

+57
-25
lines changed

5 files changed

+57
-25
lines changed

src/hooks/useStyles/useStyles.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import { mergeObjects } from '@/utils/mergeObjects/mergeObjects';
55
/**
66
* @version This hook has a upper version, please use useStylesV2
77
*/
8-
export const useStyles = <T, V = undefined | string>(
8+
export const useStyles = <T extends object, V = undefined | string>(
99
styleName: string,
1010
typeName?: V,
11-
customTokens?: object
11+
customTokens?: Partial<T>
1212
): T => {
1313
const theme = useTheme();
1414
const style = theme[styleName];
@@ -21,7 +21,7 @@ export const useStyles = <T, V = undefined | string>(
2121
if (styleName !== undefined && typeName !== undefined) {
2222
if (style?.[typeName]) {
2323
if (customTokens) {
24-
styles = mergeObjects(structuredClone(style[typeName]), customTokens);
24+
styles = mergeObjects<T, Partial<T>>(structuredClone(style[typeName]), customTokens);
2525
} else {
2626
styles = style[typeName];
2727
}

src/hooks/useStyles/useStylesV2.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,19 @@ import { useTheme } from 'styled-components';
22

33
import { mergeObjects } from '@/utils';
44

5-
interface IUseStyles<V> {
5+
interface IUseStyles<T, V> {
66
styleName: string;
77
variantName?: V;
8-
customTokens?: object;
8+
customTokens?: Partial<T>;
99
isOptional?: boolean;
1010
}
1111

12-
export const useStylesV2 = <T, V = undefined | string>({
12+
export const useStylesV2 = <T extends object, V = undefined | string>({
1313
styleName,
1414
variantName,
1515
customTokens,
1616
isOptional,
17-
}: IUseStyles<V>): T | undefined => {
17+
}: IUseStyles<T, V>): T | undefined => {
1818
const theme = useTheme();
1919
if (styleName in theme) {
2020
const style = theme[styleName];
@@ -23,7 +23,7 @@ export const useStylesV2 = <T, V = undefined | string>({
2323
return style as T;
2424
} else if ((variantName as string) in style) {
2525
if (customTokens) {
26-
return mergeObjects(structuredClone(style[variantName]), customTokens) as T;
26+
return mergeObjects<T, Partial<T>>(structuredClone(style[variantName]), customTokens) as T;
2727
}
2828
return style[variantName];
2929
}

src/utils/mergeObjectStyles/mergeObjectStyles.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
export const mergeObjectsStyles = (target: object, source: object): object => {
1+
export const mergeObjectsStyles = <T extends object, S extends object>(
2+
target: T,
3+
source: S
4+
): Partial<T & S> => {
5+
// This condition is necessary to avoid the error: "Maximum call stack size exceeded"
26
if (typeof target !== 'object' || typeof source !== 'object') {
37
return source;
48
}
@@ -7,9 +11,12 @@ export const mergeObjectsStyles = (target: object, source: object): object => {
711
for (const key in source) {
812
if (Object.prototype.hasOwnProperty.call(source, key)) {
913
if (Object.prototype.hasOwnProperty.call(target, key)) {
10-
mergedObject[key] = mergeObjectsStyles(target[key], source[key]);
14+
mergedObject[key as string] = mergeObjectsStyles<T, S>(
15+
target[key as string] as T,
16+
source[key] as S
17+
);
1118
} else {
12-
mergedObject[key] = source[key];
19+
mergedObject[key as string] = source[key];
1320
}
1421
}
1522
}

src/utils/mergeObjects/__tests__/mergeObjects.test.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,46 @@ import { mergeObjects } from '../mergeObjects';
22

33
describe('mergeObjects', () => {
44
it('should merge multiple objects into one', () => {
5-
const target = { a: 1 };
6-
const source1 = { b: 2 };
7-
const source2 = { c: 3 };
5+
type TestObjectA = { a: number };
6+
type TestObjectB = { b: number };
7+
type TestObjectC = { c: number };
8+
type TestObjectBC = Partial<TestObjectB & TestObjectC>;
89

9-
const result = mergeObjects(target, source1, source2);
10+
const target: TestObjectA = { a: 1 };
11+
const source1: TestObjectB = { b: 2 };
12+
const source2: TestObjectC = { c: 3 };
13+
14+
const result = mergeObjects<TestObjectA, TestObjectBC>(target, source1, source2);
1015

1116
expect(result).toEqual({ a: 1, b: 2, c: 3 });
1217
});
1318

1419
it('should overwrite properties with the same key', () => {
20+
type TestObject = { a: number; b: number };
21+
type TestObjectB = { b: number };
22+
type TestObjectC = { c: number };
23+
type TestObjectBC = Partial<TestObjectB & TestObjectC>;
24+
1525
const target = { a: 1, b: 2 };
1626
const source1 = { b: 3 };
1727
const source2 = { c: 4 };
1828

19-
const result = mergeObjects(target, source1, source2);
29+
const result = mergeObjects<TestObject, TestObjectBC>(target, source1, source2);
2030

2131
expect(result).toEqual({ a: 1, b: 3, c: 4 });
2232
});
2333

2434
it('should handle nested objects', () => {
35+
type TestObject = { a: { b: number } };
36+
type TestObjectTwo = { a: { c: number } };
37+
type TestObjectD = { d: number };
38+
type TestObjectTwoD = Partial<TestObjectTwo & TestObjectD>;
39+
2540
const target = { a: { b: 1 } };
2641
const source1 = { a: { c: 2 } };
2742
const source2 = { d: 3 };
2843

29-
const result = mergeObjects(target, source1, source2);
44+
const result = mergeObjects<TestObject, TestObjectTwoD>(target, source1, source2);
3045

3146
expect(result).toEqual({ a: { b: 1, c: 2 }, d: 3 });
3247
});
Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
type MergeValues<S> = S[Extract<keyof S, string>];
2+
type ParsingObject<S> = MergeValues<S> extends object ? MergeValues<S> : never;
13
/* eslint-disable complexity */
2-
export const mergeObjects = (target: object, ...sources: object[]): object => {
3-
const result = Array.isArray(target) ? [] : { ...target };
4+
export const mergeObjects = <T extends object, S extends object>(
5+
target: T,
6+
...sources: S[]
7+
): Partial<T & S> => {
8+
const result = Array.isArray(target) ? ([] as unknown as T[]) : { ...target };
49

510
for (const source of sources) {
611
for (const key in source) {
@@ -10,17 +15,22 @@ export const mergeObjects = (target: object, ...sources: object[]): object => {
1015
if (
1116
// eslint-disable-next-line no-prototype-builtins
1217
!result.hasOwnProperty(key) ||
13-
typeof result[key] !== 'object' ||
14-
result[key] === null
18+
typeof (result as S)[key] !== 'object' ||
19+
(result as S)[key] === null
1520
) {
16-
result[key] = Array.isArray(source[key]) ? [] : {};
21+
(result as S)[key] = Array.isArray(source[key])
22+
? ([] as MergeValues<S>)
23+
: ({} as MergeValues<S>);
1724
}
18-
result[key] = mergeObjects(result[key], source[key]);
25+
(result as S)[key] = mergeObjects<ParsingObject<S>, S>(
26+
(result as S)[key] as ParsingObject<S>,
27+
source[key] as S
28+
) as MergeValues<S>;
1929
} else {
20-
result[key] = source[key];
30+
(result as S)[key] = source[key];
2131
}
2232
}
2333
}
2434
}
25-
return result;
35+
return result as Partial<T & S>;
2636
};

0 commit comments

Comments
 (0)