Reputation: 91
I'm trying to observe properties of objects in arrays. To make it type safe I'm using a getter function to get to the child object of the array objects (if necessary) that contain the actual property to be observed.
The propertyName / key
must a string as the observe library I'm using needs it.
The getter should be able to accept a function that returns the same type as was passed in, like o => o
.
You can find a condensed example below:
function foo<A, B>(
array: A[],
getter: (ao: A) => B,
key: keyof B,
callback: (value: B[keyof B]) => void
) {
callback(getter(array[0])[key]);
}
foo([{b: {c: 1}}], a => a.b, "c", v => {});
Alas this throws Argument of type '"c"' is not assignable to parameter of type 'never'.
But the following does work:
function bar<A>(
array: A[],
key: keyof A,
callback: (value: A[keyof A]) => void
) {
callback(array[0][key]);
}
bar([{b: 1}], "b", v => {});
Why is the compiler not able to infer the type of B
and is there a workaround I could use?
Upvotes: 1
Views: 908
Reputation: 327624
I don't have a definitive answer as to why the compiler doesn't infer B
. My intuition is that it's much easier for the compiler to infer the type of a parameter you actually pass to the function, than it is for it to infer a type only related to the parameter you pass in. Based on that intuition, I'd rewrite the types to replace B
with Record<K,V>
, where K
is the key you are actually passing in, and V
is the type of value of the property associated with that key.
In case you're not aware, Record<K,V>
is part of the TypeScript standard library and is defined like this:
type Record<K extends string, T> = {
[P in K]: T;
}
Then the function becomes:
function foo<A, K extends string, V>(
array: A[],
getter: (ao: A) => Record<K,V>,
key: K, // easier to infer K because you directly pass it in
callback: (value: V) => void
) {
callback(getter(array[0])[key]);
}
And this seems to work:
foo([{ b: { c: 1 } }], a => a.b, "c", v => { });
The parameters are inferred as <{ b: { c: number; }; }, "c", number>
as desired.
Hope that helps; good luck!
@TitianCernicovaDragomir points out that the following call:
foo([{ b: { c: 1, d: "" } }], a => a.b, "c", v => { });
gets inferred as
foo<{ b: { c: number; d: string; }; }, "c" | "d", string | number>(...)
The problem here is that you want K
to be just "c"
, and not "c" | "d"
, but apparently the array
parameter was inspected before the key
parameter. (I don't think the fact that V
is string | number
is the actual problem, since callback
in fact takes any value whatsoever. If you passed a callback
function that only took number
and it failed, then that would be a problem.)
If you find that some type parameters are inferred in the wrong order (meaning the compiler uses a parameter to infer something, but you would rather it used a different parameter) you can lower the priority of an inference by intersecting the type with {}
.
In this case, we can do this:
function foo<A, K extends string, V>(
array: A[],
getter: (ao: A) => Record<K & {}, V>, // delay inference of K
key: K,
callback: (value: V) => void
) {
callback(getter(array[0])[key]);
}
Now when we call
foo([{ b: { c: 1, d: "" } }], a => a.b, "c", v => { });
it is inferred as
foo<{ b: { c: number; d: string; }; }, "c", {}>(...)
which works fine, since the callback doesn't do anything. If we call
foo([{ b: { c: 1, d: "" } }], a => a.b, "c", (v: number): void => { });
then it is inferred as
foo<{ b: { c: number; d: string; }; }, "c", number>(...)
which is fine, right?
Any other use cases we need to support?
Upvotes: 2
Reputation: 249466
Similar to @jcalz I don't necessarily have a reason, just a workaround. If you do it in a two call approach, you can fix A
and B
in a first call, and send the key
and the callback
in the second call and then the types will be inferred correctly.
function foo2<A, B>(
array: A[],
getter: (ao: A) => B,
) {
return function <K extends keyof B>(key: K, callback: (value: B[K]) => void) {
callback(getter(array[0])[key]);
}
}
foo2([{ b: { c: 1, d: "" } }], a => a.b)("d", v => { }); // v is string
foo2([{ b: { c: 1, d: "" } }], a => a.b)("c", v => { }); // v is number
It's a bit uglier as far as syntax goes but you get full type safety.
Upvotes: 1