Reputation: 61
I've been using Typescript for a few months now, but am stuck with getting my index signatures to work properly.
sensorTestData = [
{
altitude: 249.74905877223617
gas: 4361
humidity: 53.16487239957308
pressure: 30.100467823211194
temperature: 69.0322421875
timestamp: "2022-02-23 00:10:12.377165"
},
{
altitude: 249.5376138919663
gas: 700
humidity: 53.15552212315537
pressure: 30.100694114206632
temperature: 69.07759374999999
timestamp: "2022-02-23 00:10:13.574070"
}
]
The array of objects look like this: { temperature: number; humidity: number; pressure: number; altitude: number; gas: number; timestamp: string; }
I've tried multiple different ways to add [metric: string]: any and can't seem to get anywhere without using "any."
Here is an example of a function I am using with the array of objects:
const getMax = (metric: string) => {
return parseFloat(
Math.max(...sensorTestData.map((obj: {
temperature: number;
humidity: number;
pressure: number;
altitude: number;
gas: number;
timestamp: string;
}) => obj[metric])).toFixed(2)
);
};
console.log("maxTemp", getMax("temperature"));
console.log("maxHumidity", getMax("humidity"));```
Error thrown: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ temperature: number; humidity: number; pressure: number; altitude: number; gas: number; timestamp: string; }'.
No index signature with a parameter of type 'string' was found on type '{ temperature: number; humidity: number; pressure: number; altitude: number; gas: number; timestamp: string; }'.ts(7053)
Upvotes: 1
Views: 2717
Reputation: 327819
In order for this to compile without error, you need the metric
parameter to be more specific than just string
. After all, if you pass some arbitrary string
in to getMax()
like getMax("oopsieDaisy")
then you'll be trying to get Math.max()
of a bunch of undefined
values (which is probably an error). The compiler error is warning you about this very issue; the compiler has no idea what might happen for an arbitrary string
. So you need to make sure that metric
is one of the known keys of the sensorTestData
elements.
In fact, it specifically has to be those keys whose value type is known to be number
; if you call getMax("timestamp")
you will be getting Math.max()
of a bunch of string
values, (which is also probably an error).
If we define the Data
interface like this to correspond to the elements of sensorTestData
,
interface Data {
temperature: number;
humidity: number;
pressure: number;
altitude: number;
gas: number;
timestamp: string;
}
Then we want metric
to be of type "temperature" | "humidity" | "pressure" | "altitude" | "gas"
(that's the union of the known string literal keys whose values in Data
are number
).
If we define a type alias for that:
type KeysForNumericData = "temperature" | "humidity" | "pressure" | "altitude" | "gas"
And we annotate the type of metric
with it:
const getMax = (metric: KeysForNumericData) => {
return parseFloat(
Math.max(...sensorTestData.map((obj) => obj[metric])).toFixed(2)
);
};
Then that compiles with no error. And you get type checking when you call getMax()
also:
getMax("temperature"); // okay
getMax("humidity"); // okay
getMax("oopsieDaisy"); // error!
// Argument of type '"oopsieDaisy"' is not assignable to
// parameter of type 'KeysForNumericData'.
getMax("timestamp"); // error!
// Argument of type '"timestamp"' is not assignable to
// parameter of type 'KeysForNumericData'.
That's basically the answer, except that if Data
ever changes, the type of KeysForNumericData
will not automatically update. It might be nice to have the compiler compute KeysForNumericData
in terms of Data
. Here's one way to do it:
type KeysMatching<T, V> = {
[K in keyof T]-?: T[K] extends V ? K : never
}[keyof T]
type KeysForNumericData = KeysMatching<Data, number>;
//type KeysForNumericData = "temperature" | "humidity" | "pressure" | "altitude" | "gas"
I've defined a utility type called KeysMatching<T, V>
(explained in this answer and this answer and probably elsewhere) which returns the keys of T
whose property values are assignable to V
.
And so to get the keys of Data
whose values are assignable to number
, we write KeysMatching<Data, number>
.
There are a few different ways to implement KeysMatching
. A brief explanation of this one: we are mapping over the keys K
of T
and for each property T[K]
at that key we are checking via conditional type if that property is assignable to number
. If so, we include that key (K
), otherwise we exclude it (with the never
type). That mapped type (everything before the [keyof T]
) for KeysMatching<Data, number>
evaluates to something like {temperature: "temperature", humidity: "humidity", ... timestamp: never}
where only the timestamp
key has a never
property value. Then we index into this mapped type with keyof T
, producing the union of property values, which in this case is "temperature" | "humidity" | ... | "gas" | never
". Since never
is an impossible type, it gets absorbed into all unions (XXX | never
is equivalent to XXX
) and so the result is the desired union of keys.
And now if Data
changes, KeysForNumericData
will automatically change to fit:
interface Data {
temperature: number;
humidity: number;
pressure: number;
altitude: number;
gas: number;
timestamp: string;
latitude: number;
longitude: number;
flavor: string;
glutenFree: boolean;
}
type KeysForNumericData = KeysMatching<Data, number>;
/* type KeysForNumericData = "temperature" | "humidity" | "pressure" |
"altitude" | "gas" | "latitude" | "longitude" */
Upvotes: 1