Reputation: 1745
I am trying to create a function similar to (but simpler than) redux's combineReducers, but I'm not quite sure how to type it. Here's the javascript
function handler1({item}) {
}
function handler2({item, otherItem}) {
}
function combineHandlers(...handlers) {
return (items) => {
handlers.forEach(handler => {
handler(items);
});
}
}
const combined = combineHandlers(handler1, handler2);
combined({item: whatever, otherItem: whatever2})
the idea is that handler1, handler2, and combineHandlers are all going to get passed an object with items as the values. The handlers will care about certain items on that top-level object, which may or may not be the same as the items the other handlers care about. The combined version will just call all of the other handlers. Nothing fancy.
I have a helper to generate the type of the function that should be returned by combineHandlers. I want to make sure that it's type safe so that given the right parameters to that helper, it would require the correct handlers passed to combineHandlers to match the type. Basically I want the following to be type safe.
type SharedType = {
shared: number;
}
type Foo = SharedType & {
stuff: number
}
type ObjWithFoo = {
foo: Foo
};
type Bar = SharedType & {
otherStuff: string
}
type ObjWithBar = {
bar: Bar;
}
function handler1(objs: ObjWithFoo): void {
}
function handler2(objs: ObjWithFoo & ObjWithBar): void {
}
type CombinedFooAndBarHandler = CombinedHandler<ObjWithFoo, ObjWithBar>;
const foo: CombinedFooAndBarHandler = combineHandlers(handler1, handler2);
It seems like CombinedFooAndBarHandler
has the proper type I am looking for. But combineHandlers has a type error. My goal is for the last line to type check as you'd expect it to (the types of the handlers need to match the generic params passed to CombinedHandler).
Here's the full version of what I have so far.
Upvotes: 0
Views: 533
Reputation: 33111
Here you have a working code:
type UnionToIntersection<U> = (
U extends unknown ? (k: U) => void : never
) extends (k: infer I) => void
? I
: never;
//just makes some types easier to read. doesn't actually do anything
type Expand<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;
type CombinedHandler<
a1 extends Record<string, SharedType> = never,
a2 extends Record<string, SharedType> = never,
a3 extends Record<string, SharedType> = never,
a4 extends Record<string, SharedType> = never,
a5 extends Record<string, SharedType> = never,
a6 extends Record<string, SharedType> = never,
a7 extends Record<string, SharedType> = never,
a8 extends Record<string, SharedType> = never,
a9 extends Record<string, SharedType> = never,
a10 extends Record<string, SharedType> = never
> = (
objs: Expand<UnionToIntersection<a1 | a2 | a3 | a4 | a5 | a6 | a7 | a8 | a9 | a10>>
) => void;
type SharedType = {
shared: number;
}
type Foo = SharedType & {
stuff: number
}
type ObjWithFoo = {
foo: Foo
};
type Bar = SharedType & {
otherStuff: string
}
type ObjWithBar = {
bar: Bar;
}
function handler1(objs: ObjWithFoo): void {
}
function handler2(objs: ObjWithFoo & ObjWithBar): void {
objs.bar.otherStuff
}
/**
* Treat it as a base type of any function
* You don't need to write types for every function
* Let TS do the job
*/
type Fn = (...args: any[]) => any
type SafeParameters<T> = T extends Fn ? Parameters<T>[0] : never
function combineHandlers<T extends Fn, Rest extends T[]>(...handlers: [...Rest]) {
return (objs: SafeParameters<UnionToIntersection<Rest[number]>>): void => {
handlers.forEach(handler => {
handler(objs);
});
}
}
type CombinedFooAndBar = CombinedHandler<ObjWithFoo, ObjWithBar>;
const fooAndBar = combineHandlers(handler1, handler2)((
{
foo: { shared: 1, stuff: 1 },
bar: { shared: 1, otherStuff: 'sd' }
}
)); // ok
const foo = combineHandlers(handler1, handler2)((
{
foo: { shared: 1, stuff: 1 },
// bar: { shared: 1, otherStuff: 'sd' }
}
)); // expected error
This line:<T extends Fn, Rest extends T[]>(...handlers: [...Rest]
allows TS to infer every passed function, so you don't need to worry about it
Upvotes: 2
Reputation: 6099
Function handler1
does not accept arguments of type ObjWithBar
. It might access stuff
, which is not present in objects of type ObjWithBar
. You combine handler1
with handler2
to get a function that handles both objects of type ObjectWithFoo
and ObjectWithBar
, but this is rejected by Typescript, because it is not type safe, because handler1
may access stuff
.
You can only combine handler functions that are guaranteed to access only shared
and not stuff
or otherStuff
. In other words, the handler functions must have an argument of a type that guarantees this.
You have to create a type ObjWithShared
and combine only handlers with an argument of type ObjWithShared
, for example as follows:
abstract class ObjWithShared {
abstract get sharedObj() : SharedType
}
class ObjWithFoo extends ObjWithShared {
foo: Foo
constructor(foo: Foo) {
super()
this.foo = foo
}
get sharedObj(): SharedType {
return this.foo
}
}
class ObjWithBar extends ObjWithShared {
bar: Bar
constructor(bar: Bar) {
super()
this.bar = bar
}
get sharedObj(): SharedType {
return this.bar
}
}
function handler1(objs: ObjWithShared): void {
console.log(`handler1 handles ${objs.constructor.name}, ${objs.sharedObj.shared}`)
}
function handler2(objs: ObjWithShared): void {
console.log(`handler2 handles ${objs.constructor.name}, ${objs.sharedObj.shared}`)
}
type Handler = (objs: ObjWithShared) => void;
function combineHandlers(...handlers: Handler[]): Handler {
return (objs: ObjWithShared) => {
handlers.forEach(handler => handler(objs))
}
}
const combinedFooAndBar: Handler = combineHandlers(handler1, handler2)
Here is some code to demonstrate the combined handler:
const foo: Foo = { shared: 111, stuff: 10}
const bar: Bar = { shared: 222, otherStuff: 'a'}
const objWithFoo = new ObjWithFoo(foo)
const objWithBar = new ObjWithBar(bar)
combinedFooAndBar(objWithFoo)
combinedFooAndBar(objWithBar)
The output is:
handler1 handles ObjWithFoo, 111
handler2 handles ObjWithFoo, 111
handler1 handles ObjWithBar, 222
handler2 handles ObjWithBar, 222
Upvotes: 1