Home > Software engineering >  TypeScript generics - Type 'T[string]' is not assignable to type 'number'
TypeScript generics - Type 'T[string]' is not assignable to type 'number'

Time:09-24

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.

Playground link to code

  • Related