For more related answers, please click on the following links: here and here
Now, when you hover over the methods.baz
, it reveals that the baz
method is a union of two functions:
((fooArg: number) => number) | ((bazArg: boolean) => (string | boolean)[])
.
If you refer to the related answers, you'll understand that invoking a function which is a union of multiple functions results in intersecting arguments.
In this scenario, it equates to number & boolean === never
.
Let's delve into the mapper
.
I suggest breaking down mapper
into two distinct strategies - mapperArray
and mapperObject
:
const functions = {
fooInternal: (fooArg: number) => 1 + fooArg,
barInternal: (barArg: string) => "barArg: " + barArg,
bazInternal: (bazArg: boolean) => ["some", "array", "with", bazArg]
} as const
type FunctionsKeys = keyof typeof functions;
type MappedFunctions = {
fooInternal: (fooArg: number) => number,
bazInternal: (bazArg: boolean) => (string | boolean)[]
}
type Values<T> = T[keyof T]
type MethodType = (...args: any[]) => any;
type Repository = Record<string, MethodType>;
const mapperArray = <Elem extends FunctionsKeys, Elems extends Elem[]>(map: [...Elems]) =>
map.reduce<Repository>((acc, elem) => ({
...acc,
[elem]: functions[elem]
}), {})
const mapperObject = <
Key extends PropertyKey,
Value extends FunctionsKeys
>(map: Record<Key, Value>) =>
(Object.entries(map) as Array<[Key, Value]>)
.reduce<Repository>((acc, elem) => ({
...acc,
[elem[0]]: functions[elem[1]]
}), {})
const mapper = <Key extends FunctionsKeys>(map: Record<string, Key> | Key[]) =>
Array.isArray(map)
? mapperArray(map)
: mapperObject(map)
Considering that the mapper
is from a third-party library, feel free to utilize only the final version. I've covered all scenarios regarding imported functions and your own, hence the detailed typing.
With our main utilities in place, we can typify our primary function:
type GetKeyBaValue<Obj, Value> = {
[Prop in keyof Obj]: Obj[Prop] extends Value ? Prop : never
}[keyof Obj]
type Test = GetKeyBaValue<{ age: 42, name: 'Serhii' }, 42> // age
const mapTyped = <
Keys extends keyof MappedFunctions,
Mapper extends <Arg extends Record<string, Keys> | Array<Keys>>(arg: Arg) => Repository
>(mpr: Mapper) => {
function anonymous<Prop extends string, Mp extends Record<Prop, Keys>>(map: Mp): {
[P in Keys as GetKeyBaValue<Mp, P>]: MappedFunctions[P]
}
function anonymous<Prop extends string, Mp extends Record<Prop, Keys>>(map: Mp) {
return mpr(map)
}
return anonymous
}
GetKeyBaValue
- retrieves the key by its corresponding value.
You might have noticed that mapTypes
is a curried function. Moreover, it returns an overloaded function named anonymous
. It could be transformed into an arrow one, but then you'd need to use type assertion with as
. In my opinion, overloading is safer, although it behaves bivariantly, resulting in some limitations. The choice is yours.
The full code snippet is provided below:
const functions = {
fooInternal: (fooArg: number) => 1 + fooArg,
barInternal: (barArg: string) => "barArg: " + barArg,
bazInternal: (bazArg: boolean) => ["some", "array", "with", bazArg]
} as const
type FunctionsKeys = keyof typeof functions;
type MappedFunctions = {
fooInternal: (fooArg: number) => number,
bazInternal: (bazArg: boolean) => (string | boolean)[]
}
type Values<T> = T[keyof T]
type MethodType = (...args: any[]) => any;
type Repository = Record<string, MethodType>;
const mapperArray = <Elem extends FunctionsKeys, Elems extends Elem[]>(map: [...Elems]) =>
map.reduce<Repository>((acc, elem) => ({
...acc,
[elem]: functions[elem]
}), {})
const mapperObject = <
Key extends PropertyKey,
Value extends FunctionsKeys
>(map: Record<Key, Value>) =>
(Object.entries(map) as Array<[Key, Value]>)
.reduce<Repository>((acc, elem) => ({
...acc,
[elem[0]]: functions[elem[1]]
}), {})
const mapper = <Key extends FunctionsKeys>(map: Record<string, Key> | Key[]) =>
Array.isArray(map)
? mapperArray(map)
: mapperObject(map)
type GetKeyBaValue<Obj, Value> = {
[Prop in keyof Obj]: Obj[Prop] extends Value ? Prop : never
}[keyof Obj]
type Test = GetKeyBaValue<{ age: 42, name: 'Serhii' }, 42> // age
const mapTyped = <
Keys extends keyof MappedFunctions,
Mapper extends <Arg extends Record<string, Keys> | Array<Keys>>(arg: Arg) => Repository
>(mpr: Mapper) => {
function anonymous<Prop extends string, Mp extends Record<Prop, Keys>>(map: Mp): {
[P in Keys as GetKeyBaValue<Mp, P>]: MappedFunctions[P]
}
function anonymous<Prop extends string, Mp extends Record<Prop, Keys>>(map: Mp) {
return mpr(map)
}
return anonymous
}
const methods = {
...mapTyped(mapper)({ baz: "bazInternal", foo: "fooInternal" }),
otherMethod: () => 42
};
methods.baz(true) // ok
methods.foo(42) // ok
methods.otherMethod() // number
Explore the code further in the Playground