I am developing a function that takes in an array of objects and resamples the array based on a property of the objects. (More theory on that in my question resampling an array of objects. In typescript, the function starts like this:
export function resample<T>(
originalArray: Array<T>,
sortKey: keyof T,
samplingInterval: number
): Array<T> {
const array = [...originalArray];
let queuedItem = array.shift();
let t0: number = queuedItem[sortKey]; // <-------- problem here
// ... more code
}
When trying to type t0
as a number, I get the error:
Type 'T[string]' is not assignable to type 'number'
TypeScript playground demonstrating the issue
Without explicitly defining the type, t0
is typed as T[keyof T]
. Which makes sense. But later in the code, I do some simple math with t0
, and I need it ot be treated as a number.
Why can T[string]
not be assignable to a number type? We have no information thus far about what's in T
, why does typescript prohibit that T[keyof T]
from being a number?
Can I better type this function, and T
? It must be that T
has at least one property whose value is a number, but whose key could be any string, and when being used in the resample
function, sortKey
is any property of T
whose value is a number?
CodePudding user response:
The issue is that you are missing constraints on the type T, that's why TS has no idea what type will be given after: queuedItem[sortKey]
To fix this, simply add constraint like this:
Pay attention to the extends
:
export function resample<T extends {[key in keyof T] : number}>(originalArray: T[], sortKey: keyof T, samplingInterval: number): Array<T> {
const array = [...originalArray];
const result: Array<T> = [];
let queuedItem = array.shift();
let t0:number = queuedItem![sortKey]; // <-------- no problem here
// ... more code
return result;
}
CodePudding user response:
My suggestion here is to make the resample
function generic both in T
, the type of the elements of originalArray
, and in K
, the type of sortKey
:
function resample<T extends Record<K, number>, K extends keyof T>(
originalArray: Array<T>,
sortKey: K,
) {
const array = [...originalArray];
let queuedItem = array.shift();
let t0: number = queuedItem![sortKey]; // no error now
}
By constraining K
to extend keyof T
we are requiring that sortKey
be one of the keys of the elements of obj
, and by constraining T
to extend Record<K, number>
(using the Record<K, V>
utility type) we are requiring that the elements of obj
have a number
value at the key K
.
Then you should be able to call resample()
and see that it works for valid calls and reports an error for invalid calls:
resample([{ a: 123, b: "hello" }], "a"); // okay
resample([{ a: 123, b: "hello" }], "b"); // error!
// ---------------> ~
// Type 'string' is not assignable to type 'number'.
Technically you don't need to do both constraints; it is sufficient to have just K
:
function resample<K extends PropertyKey>(
originalArray: Array<Record<K, number>>,
sortKey: K,
) { /* same impl */ }
But then you run into excess property checking where the compiler complains when object literals have properties that it wasn't expecting:
resample([{ a: 123, b: "hello" }], "a"); // error?!
// ---------------> ~~~~~~~~~~
// Object literal may only specify known properties,
// and 'b' does not exist in type 'Record<"a", number>'
You don't want this behavior because it really isn't indicative of an error that your object has more keys than "a"
. This can be worked around or avoided by not calling resample()
with object literal arguments, by initializing a variable with the object literal and then calling resample()
with that variable:
const differentVariable = { a: 123, b: "hello" };
resample([differentVariable], "a"); // okay
But one way to turn off such checking is to make the elements of obj
of a generic type T extends Record<K, number>
instead of the specific type Record<K, number>
. Which is what I suggest above.