Why does the argument for Array.prototype.includes(searchElement) need the same type as array elements?
Yes, technically it should be safe to allow the searchElement
parameter in Array<T>.includes()
to be a supertype of T
, but the standard TypeScript library declaration assumes that it is just T
. For most purposes, this is a good assumption, since you don't usually want to compare completely unrelated types as @GustavoLopes mentions. But your type isn't completely unrelated, is it?
There are different ways to deal with this. The assertion you've made is probably the least correct one because you are asserting that a string
is an AllowedChars
even though it might not be. It "gets the job done" but you're right to feel uneasy about it.
Another way is to locally override the standard library via declaration merging to accept supertypes, which is a bit complicated and uses conditional types:
// remove "declare global" if you are writing your code in global scope to begin with
declare global {
interface Array<T> {
includes<U extends (T extends U ? unknown : never)>(searchElement: U, fromIndex?: number): boolean;
}
}
Then your original code will just work:
if (exampleArr.includes(e.key)) {} // okay
// call to includes inspects as
// (method) Array<AllowedChars>.includes<string>(searchElement: string, fromIndex?: number | undefined): boolean (+1 overload)
while still preventing the comparison of completely unrelated types:
if (exampleArr.includes(123)) {} // error
// Argument of type '123' is not assignable to parameter of type 'AllowedChars'.
But the easiest and still kind-of-correct way to deal with this is to widen the type of exampleArr
to string[]
:
const stringArr: string[] = exampleArr; // no assertion
if (stringArr.includes(e.key)) {} // okay
Or more succinctly like:
if ((exampleArr as string[]).includes(e.key)) {} // okay
Widening to string[]
is only "kind of" correct because TypeScript unsafely treats Array<T>
as covariant in T
for convenience. This is fine for reading, but when you write properties you run into problems:
(exampleArr as string[]).push("whoopsie"); // uh oh
But since you're just reading from the array it's perfectly safe.
Playground link to code
If you are comparing two different types, then they are naturally different.
Imagine you have:
type A = {paramA: string};
type B = {paramB: number};
const valuesA: A[] = [{paramA: 'whatever'}];
const valueB: B = {paramB: 5};
valuesA.includes(valueB); // This will always be false, so it does not even make sense
In your case, the compiler threats AllowedChars
as a completely different type from string
. You have to "cast" the string
you are receiving to AllowedChars
.
But how is that correct, I am expecing user input which could be anything.
The compiler has no idea what you're trying to accomplish with the includes
. It only knows they have different types, therefore they shouldn't be compared.