The short answer here is: you pretty much need to use a type assertion because it's not possible to have the compiler figure out that what you're doing is safe.
The much longer answer: in order to even begin to let the compiler know what's going on, you need the callback to be generic. Here's one way to type it:
const cb = <K extends keyof IMyInterface, T extends Partial<IMyInterface>>(
acc: T, val: K): T & Record<K, number> => ({ ...acc, [val]: 1 })
That type signature says that the cb
takes two parameters, acc
and val
. The acc
parameter is of generic type T
which must be assignable to Partial<IMyInterface>
, and the val
parameter is of generic type K
which must be assignable to keyof IMyInterface
. Then the output of the callback is T & Record<K, number>
: that is, it is an object with all the keys and values from T
, but it also has a definite number
value at the key K
. So when you call cb()
, the return value is potentially of a different type from that of acc
.
This gives enough information to the compiler to allow you to avoid type assertions... but only if you perform the reduce()
-like operation with cb()
manually, by unrolling the loop into a bunch of nested calls:
const result: IMyInterface = cb(cb(cb({}, "a"), "b"), "c"); // okay
const stillOkay: IMyInterface = cb(cb(cb({}, "a"), "c"), "b"); // okay
const mistake: IMyInterface = cb(cb(cb({}, "b"), "b"), "c"); // error! property "a" is missing
Here you can see that the compiler is really looking out for you, since if you call cb()
in the wrong way, you get an error telling you so.
Unfortunately, the type signature for Array<T>.reduce()
,
reduce<U>(
callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: readonly T[]) => U,
initialValue: U
): U;
is insufficient to represent the successive type narrowing that happens each time callbackfn
is called on elements of the array. And as far as I can tell, there's no way to alter it to do this. You want to say that the callbackfn
type is some crazy intersection of types corresponding to how it behaves for each successive member of the array, like ((p: A, c: this[0])=>B) & ((p: B, c: this[1])=>C) & ((p: C, c: this[2])=>D) & ...
, for generic parameters A
, B
, C
, D
, etc., and then hope that the compiler can infer these parameters from your call to reduce()
. Well, it can't. The kind of higher order inference just isn't part of the language (at least as of TS3.7).
So, that's where we have to stop. Either you can unroll the loop and call cb(cb(cb(...
, or you call reduce()
and use a type assertion. I think the type assertion really isn't all that bad; it's meant specifically for situations in which you are smarter than the compiler... and this seems to be one of those times.
Okay, hope that helps; good luck!
Link to code