0

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')

enter image description here

Taha Azzabi
  • 2,392
  • 22
  • 31
  • 1
    While it might be possible to generate the according types, I'd suggest solving the assignment by casting in a way which disables type-safety, along with a comment. In the end, the TS type-system is there to catch developer mistakes (erased in runtime anyways), and save you time otherwise spent debugging. In this situation, the function is short, the "unsafe" types wouldn't leak, and overall, you'll spend less time, and have less headaches about oddly complex types. Again, it might be possible, but probably make the code less readable, and slow down overall progress, when refactoring. – ASDFGerte Sep 26 '22 at 15:12
  • If desired, typing `path` to something that's surely a valid path in the object, may also be possible, but cause you a lot of headache, when calling the function with non-literal strings. Also, you might consider not re-implementing the wheel, and looking e.g. at https://lodash.com/docs/#set – ASDFGerte Sep 26 '22 at 15:13

1 Answers1

1

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

Playground

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