2

Given the following:

type Arr = (0|1)[]
const arr: Arr = [0, 1]
const someNumber: number = 5;
arr.includes(someNumber);
//           ^^^^^^^^^^
// Argument of type 'number' is not assignable to parameter of type '0 | 1'.

Playground

Is there a way to make arr.includes(someNumber) work for TypeScript without changing the definition of Arr or doing some type casting?

Doesn't this error defeat the purpose of using Array.prototype.includes()?

The includes() method determines whether an array includes a certain value among its entries, returning true or false as appropriate.

2
  • Are you looking for .filter? Commented Apr 30, 2022 at 14:57
  • @Vega, I'm not, thanks! I'm looking for "a way to make arr.includes(someNumber) work for TypeScript without changing the definition of Arr or doing some type casting?" as stated on the question. Commented Apr 30, 2022 at 15:12

3 Answers 3

3

Doesn't this error defeat the purpose of using Array.prototype.includes()?

Not really. It's just TypeScript doing its job: making the code typesafe. You've said that arr can only include the values 0 or 1, so calling includes with a value that can't be in the array is a type error. (That said, I could definitely see an argument for includes to accept number instead of 0 | 1 in a case like that. I suspect there are counter-arguments though.)

You could write a utility function to check if a number is a valid element for arr:

const isValidArrElement = (value: number): value is Arr[number] => {
    return value === 0 || value === 1;
};

Then use that before using includes:

const arr: Arr = [0, 1]
const someNumber: number = 5;
if (isValidArrElement(someNumber) && arr.includes(someNumber)) {
    // ...
}

Playground link

But I usually prefer to approach this the other way so that I'm not writing the 0 and 1 in two places and creating a maintenance hazard:

const validArrElements = [0, 1] as const;
type ArrElement = (typeof validArrElements)[number];
//   ^? -- type is 0 | 1

const isValidArrElement = (value: number): value is ArrElement => {
    return (validArrElements as readonly number[]).includes(value);
};

Note that isValidArrElement does have a type assertion in it. I'm not bothered about that, because I only do this in these pairs of things (constant arrays of valid values and type checkers for them).

The usage is the same as above:

const arr: Arr = [0, 1]
const someNumber: number = 5;
if (isValidArrElement(someNumber) && arr.includes(someNumber)) {
    // ...
}

Playground link

Sign up to request clarification or add additional context in comments.

3 Comments

But a number can be in the array (the variable is typed number, not 5). You can check a value of type 0|1 for equality with a number, as long as the types intersect Typescript won't complain - it's strange that includes does not follow the same principle.
@riv - Yeah, it's a bit subtle. It's the difference is between assignment and comparison. Passing an argument to a function is an assignment (you're assigning the argument value to the function parameter), so just like you can't assign number to, say, let x: 0 | 1;, you can't assign number to a parameter of type 0 | 1 (tsplay.dev/NnjJkN). This gets raised regularly in the TS issue list; they get directed to ts/14520, a long-standing open issue that would make it possible to write the includes signature differently.
(Or maybe I should say easier rather than possible. But given how much this comes up, I suspect there's a reason Array isn't written differently with the tools TypeScript currently has. I don't know what it is. And it wouldn't surprise me if it's partially historical -- TypeScript is much more powerful now than when types were first overlaid on Array, and adding any breaking changes is something they don't do lightly.)
1

You defined a union type consisting of 0 | 1 which means your array values will be type-guarded with value 0 or 1. That's how union types work in Typescript.

The usual array will be defined with a primitive type like below

const arr: Array<number> = [0, 1]

That is one of choices

But if you still want to go with your union type definition, you can cast it to number[] which allows all numbers being used in array's functions

type Arr = (0|1)[]
const arr: Arr = [0, 1]
const someNumber: number = 5;
(arr as readonly number[]).includes(someNumber) //correct

const someString: string = "5";
(arr as readonly number[]).includes(someString) //error

Playground

2 Comments

Thanks! I know I can solve this by changing the definition of arr or doing type casting, that's why I asked: "Is there a way to make arr.includes(someNumber) work for TypeScript without changing the definition of Arr or doing some type casting?"
Oh, perhaps I missed that part due to wording. By the way, I think you cannot achieve it without typecasting, and it's temporary on that condition only. After that condition, arr is still arr and nothing changes. @MauricioRobayo
-1
let scores : (string | number)[];
scores = ['Programming', 5, 'Software Design', 4]; 
const someNumber: number = 5
console.log('scores.includes(someNumber)', scores.includes(someNumber))

It's returing True

1 Comment

Thanks! but this is changing the definition of arr, doesn't answer my question at all: "Is there a way to make arr.includes(someNumber) work for TypeScript without changing the definition of Arr or doing some type casting?"

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.