Skip to content

Commit dd380da

Browse files
authored
fix: improve chainEventHandlers type
support spreading/passing more than 8 arguments and make the return type more accurate
1 parent aa41e16 commit dd380da

File tree

2 files changed

+91
-85
lines changed

2 files changed

+91
-85
lines changed

src/chainEventHandlers.ts

Lines changed: 39 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,88 +1,42 @@
1-
/* eslint-disable @typescript-eslint/ban-types */
2-
export function chainEventHandlers<T1 extends {}, T2 extends {}>(
3-
t1: T1,
4-
t2: T2
5-
): T1 & T2
6-
export function chainEventHandlers<T1 extends {}, T2 extends {}, T3 extends {}>(
7-
t1: T1,
8-
t2: T2,
9-
t3: T3
10-
): T1 & T2 & T3
11-
export function chainEventHandlers<
12-
T1 extends {},
13-
T2 extends {},
14-
T3 extends {},
15-
T4 extends {}
16-
>(t1: T1, t2: T2, t3: T3, t4: T4): T1 & T2 & T3 & T4
17-
export function chainEventHandlers<
18-
T1 extends {},
19-
T2 extends {},
20-
T3 extends {},
21-
T4 extends {},
22-
T5 extends {}
23-
>(t1: T1, t2: T2, t3: T3, t4: T4, t5: T5): T1 & T2 & T3 & T4 & T5
24-
export function chainEventHandlers<
25-
T1 extends {},
26-
T2 extends {},
27-
T3 extends {},
28-
T4 extends {},
29-
T5 extends {},
30-
T6 extends {}
31-
>(t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6): T1 & T2 & T3 & T4 & T5 & T6
32-
export function chainEventHandlers<
33-
T1 extends {},
34-
T2 extends {},
35-
T3 extends {},
36-
T4 extends {},
37-
T5 extends {},
38-
T6 extends {},
39-
T7 extends {}
40-
>(
41-
t1: T1,
42-
t2: T2,
43-
t3: T3,
44-
t4: T4,
45-
t5: T5,
46-
t6: T6,
47-
t7: T7
48-
): T1 & T2 & T3 & T4 & T5 & T6 & T7
49-
export function chainEventHandlers<
50-
T1 extends {},
51-
T2 extends {},
52-
T3 extends {},
53-
T4 extends {},
54-
T5 extends {},
55-
T6 extends {},
56-
T7 extends {},
57-
T8 extends {}
58-
>(
59-
t1: T1,
60-
t2: T2,
61-
t3: T3,
62-
t4: T4,
63-
t5: T5,
64-
t6: T6,
65-
t7: T7,
66-
t8: T8
67-
): T1 & T2 & T3 & T4 & T5 & T6 & T7 & T8
68-
export function chainEventHandlers(
69-
first: Record<string, any>,
70-
...rest: Record<string, any>[]
71-
): Record<string, any> {
72-
const result: Record<string, any> = { ...first }
73-
for (const obj of rest) {
74-
for (const key in obj) {
75-
const value = obj[key]
76-
const prev = result[key]
77-
if (typeof prev === 'function' && typeof value === 'function') {
78-
result[key] = (...args: any[]) => {
79-
prev(...args)
80-
return value(...args)
1+
type Assign<A, B> = {
2+
[K in keyof A | keyof B]:
3+
K extends keyof A
4+
? K extends keyof B
5+
? NonNullable<A[K]> extends (...args: any[]) => any
6+
? NonNullable<B[K]> extends (...args: any[]) => any
7+
? A[K] | B[K]
8+
: B[K]
9+
: B[K]
10+
: A[K]
11+
: K extends keyof B
12+
? B[K]
13+
: never
14+
}
15+
16+
type MergeObjectsArray<T extends object[]> = T extends [infer F, ...infer R]
17+
? Assign<F, R extends object[] ? MergeObjectsArray<R> : NonNullable<unknown>>
18+
: NonNullable<unknown>;
19+
20+
type PrettyObject<T> = { [K in keyof T]: T[K] } & NonNullable<unknown>
21+
22+
export function chainEventHandlers<First extends Record<string, any>, Rest extends Record<string, any>[]>(
23+
first: First,
24+
...rest: Rest
25+
): PrettyObject<MergeObjectsArray<[First, ...Rest]>> {
26+
const result: Record<string, any> = {...first}
27+
for (const obj of rest) {
28+
for (const key in obj) {
29+
const value = obj[key]
30+
const prev = result[key]
31+
if (typeof prev === 'function' && typeof value === 'function') {
32+
result[key] = (...args: any[]) => {
33+
prev(...args)
34+
return value(...args)
35+
}
36+
} else {
37+
result[key] = value
38+
}
8139
}
82-
} else {
83-
result[key] = value
84-
}
8540
}
86-
}
87-
return result
41+
return result as MergeObjectsArray<[First, ...Rest]>
8842
}

test/chainEventHandlers.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,55 @@ describe(`chainEventHandlers`, function () {
5353
])
5454
})
5555
})
56+
57+
/* eslint-disable @typescript-eslint/no-unused-vars */
58+
function typeTest() {
59+
{
60+
const { onClick } = chainEventHandlers(
61+
{ onClick: (a: number) => {} },
62+
{ onClick: 'foo' }
63+
)
64+
// @ts-expect-error string isn't callable
65+
onClick()
66+
// @ts-expect-error string isn't callable
67+
onClick(1)
68+
}
69+
{
70+
const { onClick } = chainEventHandlers(
71+
{ onClick: 'foo' },
72+
{ onClick: (a: number) => {} }
73+
)
74+
// @ts-expect-error missing argument
75+
onClick()
76+
onClick(1)
77+
}
78+
{
79+
const { onClick } = chainEventHandlers(
80+
{ onClick: (a: number) => {} },
81+
{ onClick: (a: string) => {} }
82+
)
83+
// @ts-expect-error signatures don't match so no typesafe call is possible
84+
onClick()
85+
// @ts-expect-error signatures don't match so no typesafe call is possible
86+
onClick(1)
87+
// @ts-expect-error signatures don't match so no typesafe call is possible
88+
onClick('a')
89+
}
90+
91+
{
92+
const { onClick } = chainEventHandlers(
93+
{ onClick: (a: string, b?: number) => {} },
94+
{ onClick: (a: string, b?: string) => {} }
95+
)
96+
onClick('a')
97+
onClick('a', undefined)
98+
// @ts-expect-error invalid type for `a` argument
99+
onClick(1)
100+
onClick(
101+
'a',
102+
// @ts-expect-error the types for the `b` argument don't match, so only `undefined` is allowed here
103+
1
104+
)
105+
}
106+
}
107+
/* eslint-enable @typescript-eslint/no-unused-vars */

0 commit comments

Comments
 (0)