I'm creating a function that receives multiple keys and values and should return an object having those keys and their respective values. The Value types should match the values I passed when I called the function.
Currently, the code works, but the typings are not exact.
I tried using the Factory way, hoping that typescript could infer something for me.
Here is the code for the factory. Also, there is a playground here.
const maker = (subModules?: Record<string, unknown>) => {
const add = <Name extends string, Q>(key: Name, value: Q) => {
const obj = {[key]: value};
return maker({
...subModules,
...obj
})
}
const build = () => {
return subModules
}
return {
add,
build
}
}
const m2 = maker()
.add('fn', ({a, b}: { a: number, b: number }) => a b)
.add('foo', 1)
.add('bar', 'aaaa')
.build()
// m2.foo -> 1
// m2.bar -> 'aaaa'
// m2.fn({a: 1, b: 2}) -> 3
m2
Also there is the option for pipeline(playground) maybe this one could be easier:
type I = <T extends any[]>(...obj: T) => { [P in T[number]['key']]: T[number]['value'] }
const metaMaker: I = <T extends any[]>(...subModules: T) => {
return subModules.reduce((acc, curr) => {
const op = {[curr.key]: curr.value}
return {
...acc,
...op
}
}, {}) as { [P in T[number]['key']]: T[number]['value'] }
}
const m = metaMaker(
{key: 'fn', value: ({a, b}: { a: number, b: number }) => a b},
{key: 'foo', value: 1},
{key: 'bar', value: 'aaaa'},
)
// m.foo -> 1
// m.bar -> 'aaaa'
// m.fn({a: 1, b: 2}) -> 3
// m
CodePudding user response:
Similar to @yeahwhat's solution, but this one has added something extra in build
. Since you are returning an intersection of many records, it can get messy pretty fast. This extends infer O
bit "collapses" the intersection into a single type.
const maker = <Submodules extends Record<string, unknown> = {}>(subModules?: Submodules) => {
const add = <Name extends string, Q>(key: Name, value: Q) => {
const obj = {[key]: value};
return maker<Submodules & Record<Name, Q>>({
...subModules,
...obj
} as Submodules & Record<Name, Q>)
}
const build = () => {
return subModules as Submodules extends infer O ? { [K in keyof O]: O[K] } : never;
}
return {
add,
build
}
}
I have also made the second option:
type Narrow<T> =
| (T extends infer U ? U : never)
| Extract<T, number | string | boolean | bigint | symbol | null | undefined | []>
| ([T] extends [[]] ? [] : { [K in keyof T]: Narrow<T[K]> });
const metaMaker = <T extends { key: string; value: any }[]>(...subModules: Narrow<T>) => {
return (subModules as T).reduce((acc, curr) => {
const op = {[curr.key]: curr.value}
return {
...acc,
...op
}
}, {}) as { [P in T[number]['key']]: Extract<T[number], { key: P }>['value'] }
}
You were pretty close with your original solution, but I have used a special type to narrow the type of the input given, without the need for using as const
. The piece you were missing is Extract<T[number], { key: P }>
to get only the specific value for that key. Before you were giving each key all the values.
CodePudding user response:
You can keep track of the initial type in a T
generic and combine it with a Record<Name, Q>
every time you add a new entry, using an intersection type, like this (playground):
const maker = <T extends Record<string, any>>(subModules?: T) => {
const add = <Name extends string, Q>(key: Name, value: Q) => {
const obj = {[key]: value} as Record<Name, Q>;
return maker<T & Record<Name, Q>>({...subModules, ...obj} as T & Record<Name, Q>)
}
const build = () => {
return subModules
}
return {
add,
build
}
}