Home > front end >  TS - cannot use object "as const" together with values every() - "This expression is
TS - cannot use object "as const" together with values every() - "This expression is

Time:04-02

I have object with hardcoded array values. This object is as const because in other part of code I need TS to by aware of all possible values.

Problem is that I cannot use .every() method on given array from that object. TS is complying about This expression is not callable. even that code works just fine.

I noticed three things while trying to debug this:

  1. Opposite method some has no problems.
  2. If I remove as const from object, there is no errors with every but like I said I cannot remove this because TS needs to be aware of all values, not treat them as any number
  3. If I use hardcoded key instead variable on that object, it works (but I need to use this as a function).

Why this does not work? What I'm doing wrong?

const demo = {
  a: [44, 23, 77],
  b: [22, 45, 55],
} as const

const everyfunction = (key: keyof typeof demo) => demo[key]
  .every((value) => value > 40) // This expression is not callable.

const somefunction = (key: keyof typeof demo) => demo[key]
  .some((value) => value > 40) // works

const everyfunction2 = () => demo['a']
  .every((value) => value > 40) // works


console.log(somefunction('a'))
console.log(everyfunction('a'))

Live example

CodePudding user response:

The extended error message provides some more clues:

Each member of the union type '...' has signatures, but none of those signatures are compatible with each other.

So why are we getting this error, and why doesn't some produce the same result? We need to look in the declaration file for Array to see what the function signatures for these methods actually are:

every<S extends T>(
  predicate: (value: T, index: number, array: T[]) => value is S, thisArg?: any
): this is S[];

every(
  predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any
): boolean;

some(
  predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any
): boolean;

As you can see, every has an overloaded version whereas some does not. The overloaded version of every acts as a type predicate, meaning that the array T passed in is asserted to be of type S. This is useful because it allows us to do things like:

const arr = ["a", "a", "a"]; // string[]

if (arr.every((char): char is "a" => char === "a")) {
  const typedArr = arr; // "a"[]
}

The problem in your example is that the type of demo[key] is [44, 23, 77] | [22, 45, 55]. This type is triggering the overloaded version of every, giving us two potential function signatures for the call:

<S extends [44, 23, 77]>(value: [44, 23, 77], index: number, array: T[]) => value is S
<S extends [22, 45, 55]>(value: [22, 45, 55], index: number, array: T[]) => value is S

The result of calling every with these signatures cannot be known by TS at compile time - they are not compatible as they contain different numeric literals. some on the other hand can be called just fine, since it has no such overload, and instead creates an intersection of the two types 44 | 23 | 77 & 22 | 45 | 55 which makes the signature valid.

The simplest way to get around this is just by asserting the tuples into a wider readonly number[] before making the every call:

const everyfunction = (key: keyof typeof demo) => (demo[key] as readonly number[])
  .every((value) => value > 40)

You could perhaps wrap this in some kind of helper function to obscure the assertion using generics (bare minimum presented here):

const everyOnObject = <O extends Record<string, readonly number[]>>(obj: O, key: keyof O) => {
  return obj[key].every((value) => value > 40);
}

console.log(everyOnObject(demo, "a"));

CodePudding user response:

Define your object structures as interfaces first, and you should be fine.

Updated example

  • Related