I am new to ts and have just started migrating my app from js to ts. first function - calcAvg, takes an array of numbers and returns their average. second function - takes a list of objects. each object has daily statistics (see DailyData interface). TS marks calcAvg(marginArray) as an error - "Argument of type '(number | null)[]' is not assignable to parameter of type 'number[]'. Type 'number | null' is not assignable to type 'number'. Type 'null' is not assignable to type 'number'.ts(2345) const marginRevArray: (number | null)[]"
Before calcAvg, I am checking the array for nulls and if there is one or more nulls values I don't calculate the average.
Any ideas what am I missing here? Thanks!
*** see code below
interface DailyData {
id: number
cost: number
margin: number
}
const calcAvg = (dataList: number[]) => {
const reducer = (accumulator: number, curr: number) => accumulator + curr;
const dataSum = dataList.reduce(reducer, 0);
return dataSum / dataList.length;
};
const avgMargin = (dailyData: DailyData[]) => {
const marginArray = dailyData.map((obj: DailyData) => obj.cost > 0 ? obj.margin/obj.cost: null);
if (marginArray.some((el: number|null) => el=== null)) {
//if the array has null values don't calculate
return 'no data';
}
return calcAvg(marginArray);
};
The TypeScript standard library call signature for Array
's some()
method looks like
interface Array<T> {
some(
predicate: (value: T, index: number, array: T[]) => unknown,
thisArg?: any
): boolean;
}
which means that when you call marginArray.some(el => el === null)
, all the compiler knows is that this will return a boolean
. It has no idea that this should have any implication whatsoever for the type of marginArray
. So the type of marginArray
is still (number | null)[]
if the call returns true
, and so the compiler balks at
calcAvg(marginArray) // error! marginArray might have nulls in it, I think
If you want to get rid of the error without refactoring , you can use a type assertion . Type assertions are meant for situations like this where you, the developer, know more about the type a value will have than the compiler can figure out. If you are sure that marginArray
will be a number[]
by the time you call calcAvg()
, then you can just tell the compiler that:
calcAvg(marginArray as number[]); // okay
This works, but is not a magic cure-all; by using a type assertion you are taking the responsibility onto yourself for its accuracy. The compiler doesn't know if marginArray
has null
s in it or not; it just believes you when you tell it. If you accidentally lie to the compiler, it won't catch it:
if (marginArray.some(el => el !== null)) { // 😅 wrong check
return 'no data';
}
return calcAvg(marginArray as number[]); // still no error
So you should double and triple check whenever you make a type assertion that you're doing it right.
On the other hand, if you don't mind some mild refactoring , you can switch from some()
to Array
's every()
method , whose standard library typings includes the following call signature :
interface Array<T> {
every<S extends T>(
predicate: (value: T, index: number, array: T[]) => value is S,
thisArg?: any
): this is S[];
}
This says that if you pass a user-defined type guard function in as predicate
which can narrow the type of the array elements, then every()
can itself be used as a user-defined type guard function which can narrow the type of the whole array. So if predicate
is annotated so that a true
return value implies that value
is number
and not null
, then a true
return value from every()
will tell the compiler that the whole array is a number[]
:
if (!marginArray.every((el: number | null): el is number => el !== null)) {
return 'no data';
}
return calcAvg(marginArray); // okay
See how the callback function has been annotated to be of type (el: number | null) => el is number
? Now the compiler understands that if calcAvg()
is reached, then marginArray
is a number[]
.
This is probably the closest you can get to type safety. I say "closest" because the act of annotating a function as a user-defined type guard is still the developer telling the compiler something it can't figure out. It would be nice if el => el !== null
would automatically be inferred as a type guard, but it's not. There's at least one open feature request about this: see microsoft/TypeScript#38390 . So you can still make the same kind of mistake as above and the compiler won't catch it:
if (!marginArray.every((el: number | null): el is number => el === null)) { // 😅
return 'no data';
}
At least in the refactored version, though, the scope of potential mistakes is narrower. You have to write el is number
right next to where you write el !== null
, whereas the original version has you writing marinArray as number[]
farther away from where the relevant check is taking place.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.