Make overProp<Obj> function interface with one param

I want to make a function overProp that would work the following way:

type Obj = {a: string, b: number}

const overA = overProp<Obj>('a', (a) => a + 'b') // takes only one type param, type of object over which it will perform

const obj = {a: 'str', b: 1}

const obj2 = overA(obj) // this would set `obj.a` to `ab`

So overProp function should limit first param to be key of Obj, second param over method should take Obj[P] where P is supplied the first prop.

The only thing I could achieve so far, so I need to duplicate a as type param and as function param:

const overA = overProp<Obj, 'a'>('a', (a) => a + 'b') 

With this implementation:

const overProp = <T, P extends keyof T>(p: P, over: (val: T[P]) => T[P]) => {
  return (obj: T) => ({
    [p]: over(obj[p]),
    ...obj
  })
}

So this is the way I thought it should work:

overProp<Obj>('a', (a) => a) // no error

overProp<Obj>('a', (a) => 'a') // no error

overProp<Obj>('b', (b) => 1) // no error

overProp<Obj>('c', ...) // error, not key 'c' in Obj

overProp<Obj>('a', (a) => 1) // error, type of key `a` is `string`

I wonder if it is possible with the latest TS version (3.5).


TypeScript doesn't currently support partial type parameter inference; if a type has two type parameters, you either have to specify both of them or the compiler will infer both of them. (Well, there are type parameter defaults but that doesn't count as inference either).

There are two workarounds I know of for this. The first is currying, where instead of having a single generic function of multiple type parameters, you have a generic function which returns another generic function. On one of them you specify the type parameter, and the other you let the compiler infer it:

const overPropCurried = <T>() => <P extends keyof T>(
  p: P,
  over: (val: T[P]) => T[P]
) => {
  return (obj: T) => ({
    [p]: over(obj[p]),
    ...obj
  });
};

const overPropObj = overPropCurried<Obj>();
overPropObj("a", a => a); // no error
overPropObj("a", a => "a"); // no error
overPropObj("b", b => 1); // no error
overPropObj("c", c => 2); // error, not key 'c' in Obj
overPropObj("a", a => 1); // error, type of key `a` is `string`

The other workaround is to use a single function which takes a dummy parameter corresponding to the type you'd like to specify. The actual value you pass as the dummy parameter doesn't matter because the function implementation ignores it... in fact, as long as the compiler thinks the value is of the right type it doesn't even have to be one at runtime:

const overPropDummy = <T, P extends keyof T>(
  dummy: T, // ignored
  p: P,
  over: (val: T[P]) => T[P]
) => {
  return (obj: T) => ({
    [p]: over(obj[p]),
    ...obj
  });
};

const dummyObj = null! as Obj; // not really an Obj but it doesn't need to be

overPropDummy(dummyObj, "a", a => a); // no error
overPropDummy(dummyObj, "a", a => "a"); // no error
overPropDummy(dummyObj, "b", b => 1); // no error
overPropDummy(dummyObj, "c", c => 2); // error, not key 'c' in Obj
overPropDummy(dummyObj, "a", a => 1); // error, type of key `a` is `string`

Either way works, and neither way is perfect. I tend to use currying myself, especially if I can use the partial result multiple times (as I've done above with overPropObj). Anyway, hope that helps; good luck!

Link to code