Am trying to type a function that update a pair key value by a given path,
am using Generics type for the object, because i do not know what type in advance and ive using also Generic type for the parameter value , but i can't figure out why ive got an error message : Type 'undefined' cannot be used as an index type
and No overload matches this call
:
const updateObjetValueByPath = <A, B>(obj: { [key: string]: A }, path: string, value: B) => {
const keys = path.split('.')
const lastKey = keys.pop()
const lastObj = keys.reduce((obj, key) => obj[key], obj)
lastObj[lastKey] = value
}
Example :
const obj = {
firstName:'Taha',
lastName: 'Az',
work:{
company:'Whitoo',
contrat:{
lib:'CDI',
}
}
}
updateObjetValueByPath(obj,'work.contrat.lib','Freelance')
CodePudding user response:
Type signature of your function is not safe, it allows you to use any path, even foo.bar.baz
fro any object, which is not safe.
Please see this example:
type Foo = {
user: {
description: {
name: string;
surname: string;
age: number
}
}
}
declare var foo: Foo;
type Primitives = string | number | symbol;
type Values<T> = T[keyof T]
type Elem = string;
type Acc = Record<string, any>
// (acc, elem) => hasProperty(acc, elem) ? acc[elem] : acc
type Predicate<Accumulator extends Acc, El extends Elem> =
El extends keyof Accumulator ? Accumulator[El] : Accumulator
type Reducer<
Keys extends Elem,
Accumulator extends Acc = {}
> =
Keys extends `${infer Prop}.${infer Rest}`
? Reducer<Rest, Predicate<Accumulator, Prop>>
: Keys extends `${infer Last}`
? Predicate<Accumulator, Last>
: never
const hasProperty = <Obj, Prop extends Primitives>(obj: Obj, prop: Prop)
: obj is Obj & Record<Prop, any> =>
Object.prototype.hasOwnProperty.call(obj, prop);
type KeysUnion<T, Cache extends string = ''> =
T extends Primitives ? Cache : {
[P in keyof T]:
P extends string
? Cache extends ''
? KeysUnion<T[P], `${P}`>
: Cache | KeysUnion<T[P], `${Cache}.${P}`>
: never
}[keyof T]
type O = KeysUnion<Foo>
type ValuesUnion<T, Cache = T> =
T extends Primitives ? T : Values<{
[P in keyof T]:
| Cache | T[P]
| ValuesUnion<T[P], Cache | T[P]>
}>
function updateObjetValueByPath<Obj, Keys extends KeysUnion<Obj>>
(obj: ValuesUnion<Obj>, path: Keys & string, value: Reducer<Keys, Obj & Acc>) {
const keys = path.split('.')
const lastKey = keys.pop()!
const lastObj = keys.reduce((o, key) => o[key], obj as Record<string, {}>)
lastObj[lastKey] = value
}
/**
* Ok
*/
const result1 = updateObjetValueByPath(foo, 'user.description.name', 'John') // ok
const result2 = updateObjetValueByPath(foo, 'user.description.age', 2) // ok
/**
* Expected errors
*/
const result3 = updateObjetValueByPath(foo, 'surname', 'Doe') // expected error, path is not full
const result4 = updateObjetValueByPath(foo, 'user.description.name', 42) // expecte derror, it should be a string
If you are interested in explanation please see my article and my answer1 , answer2, answer3, answer4
As for missing link, please see links on the very bottom of my article https://catchts.com/deep-pick.
P.S. "add comment" button does not work on my mobile phonr