简体   繁体   中英

Argument of type '(number | null)[]' is not assignable to parameter of type 'number[]'

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.


Playground link to code

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM