General Typescript question say I iterate over an array which i know its content and apply a reduce to get an object back which I do know the type for instance:
interface IMyInterface {
a: number;
b: number;
c: number;
}
const result: IMyInterface = ['a','b','c'].reduce((acc: Partial<IMyInterface>,val)=>({...acc,[val]: 1}), {});
Now that wont work because result is expected to be Partial<IMyInterface>
which makes sense, considereing TS cant tell the content of the array will produce the "full" object.
However What do I need to do so that result can be of type IMyInterface without the need of as IMyInterface
?
Here is a repl https://repl.it/@Sudakatux/KaleidoscopicGraciousApplicationpackage
Thanks in advance
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.
Link to code
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With