Why does the argument for Array.prototype.includes(searchElement) need the same type as array elements?
See microsoft/TypeScript#26255 for a full discussion on Array.prototype.includes()
and supertypes.
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 because TypeScript doesn't support supertype constraints (see ms/TS#14520 for the feature request). Instead, the declaration uses conditional types to emulate a supertype constraint:
// 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 correct way to deal with this is to widen the type of exampleArr
to readonly string[]
:
const stringArr: readonly string[] = exampleArr; // no assertion
if (stringArr.includes(e.key)) {} // okay
Or more succinctly like:
if ((exampleArr as readonly string[]).includes(e.key)) {} // okay
Widening to readonly string[]
is fine, but be careful widening to string[]
, which is a little more dangerous 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, don't do this
But since you're just reading from the array it's perfectly safe, and why readonly
is recommended.
Playground link to code
Why does a tuple union expect `never` as `.includes()` argument?
Let's take a look at some of the types. We have
array: readonly ["foo"] | readonly ["bar"] | readonly ["bar", "baz"]
Now, the include
method on a T[]
takes an argument of type T
, i.e. an argument of the correct type for the list. This means that, in Typescript, it's not well-typed to ask whether a string is an element of an array of integers, as that sort of code is likely a mistake to begin with.
Now, we don't have an array. We have a union. You can only call a method on a union if the method is applicable to all of the union options.
["foo"]
has a method include
which takes a "foo"
as argument (that is, the type whose only inhabitant is the literal string "foo"
. Likewise, ["bar"]
has a method include
which takes a "bar"
as argument, and ["bar", "baz"]
has a include
which takes an element of the type "bar" | "baz"
as argument.
But we don't know which of these we have. We could have any of them. So our include
has to take something that would be valid for all three of these. That is, our include
must take an argument of type
"foo" & "bar" & "baz"
So we can pass any string which is equal to all three of the above strings. I don't know of any such string, and neither does the Typescript compiler, so it rightly says that that type is uninhabited, i.e. never
. There is no type-safe way to call include
.
You can any
-cast your way around it, like you can everything in Typescript. But the smarter technique is to maintain type safety. See, Typescript is inferring a type too specific for your array
variable, a common problem when you start dealing with literal types. But it seems you want the type to be
array : readonly Word[]
which, as it happens, is a perfectly valid supertype of of the type I indicated at the top of this post. We can simply use an explicit type declaration to make the code work
const array: readonly Word[] = schema[schemaKey]
Note that this is not a cast. We're not telling the type system "I know better than you, hush". readonly Word[]
is a perfectly valid supertype of the literal union Typescript was inferring. This code is still typechecked and typesafe. We just had to give the compiler a little hint as to our intentions.
TypeScript const assertions: how to use Array.prototype.includes?
The standard library signature for Array<T>.includes(u)
assumes that the value to be checked is of the same or narrower type than the array's elements T
. But in your case you are doing the opposite, checking against a value which is of a wider type. In fact, the only time you would say that Array<T>.includes<U>(x: U)
is a mistake and must be prohibited is if there is no overlap between T
and U
(i.e., when T & U
is never
).
Now, if you're not going to be doing this sort of "opposite" use of includes()
very often, and you want zero runtime efects, you should just widen CAPITAL_LETTERS
to ReadonlyArray<string>
via type assertion:
(CAPITAL_LETTERS as ReadonlyArray<string>).includes(str); // okay
If, on the other hand, you feel seriously enough that this use of includes()
should be accepted with no type assertions, and you want it to happen in all of your code, you could merge in a custom declaration:
// global augmentation needed if your code is in a module
// if your code is not in a module, get rid of "declare global":
declare global {
interface ReadonlyArray<T> {
includes<U>(x: U & ((T & U) extends never ? never : unknown)): boolean;
}
}
That will make it so that an array (well, a readonly array, but that's what you have in this example) will allow any parameter for .includes()
as long as there is some overlap between the array element type and the parameter type. Since string & CapitalLetter
is not never
, it will allow the call. It will still forbid CAPITAL_LETTERS.includes(123)
, though.
Okay, hope that helps; good luck!
TypeScript: Restrict function parameter value to values of array within dynamic variable
You want the compiler to keep track of the literal type of the strings passed in as the paths
property. Unfortunately this did not happen with a generic constraint like P extends string[]
. One way to increase the likelihood that the compiler will treat "foo"
as being of type "foo"
instead of type string
is to constrain a generic type parameter to string
. So P extends string
will work better. We can just have P
be the type of the elements of paths
instead of paths
itself, like this:
export const useLayoutPaths = <P extends string>(options: Options<P[]>) => {
// impl
};
This will work as desired, but the compiler doesn't like letting you look up a string
in an array of P
. See this question and answer for more information. The easiest way to deal with that is to widen paths
from P[]
to readonly string[]
before using includes()
:
export const useLayoutPaths = <P extends string>(options: Options<P[]>) => {
const { pathname, rootPath, paths } = options;
if (paths.length === 0) {
throw new Error("paths must be a non-empty array.");
}
const currentPath = pathname.substring(pathname.lastIndexOf("/") + 1);
const value = (paths as readonly string[]).includes(currentPath) ? currentPath : paths[0];
const getPath = (name: typeof paths[number]): string =>
`${rootPath}/${String(name)}`;
return { value, getPath };
};
And now things work as you want:
console.log(getPath("trends")) // okay
getPath("oops"); // error!
Playground link to code
Alternative to array.includes() in google apps script?
EDIT(2019-04-07): Please be advised. With the expected V8 upgrade (ECMAScript 2017) for App Script, the language will natively support Array.prototype.includes
and many other modern Javascript features in the near future.
The simplest solution for array.prototype.includes
is to use the following polyfill from MDN in your apps script project. Just create a script file and paste the following code - the code/polyfill will add the function directly to the Array prototype object:
// https://tc39.github.io/ecma262/#sec-array.prototype.includes
if (!Array.prototype.includes) {
Object.defineProperty(Array.prototype, 'includes', {
value: function(searchElement, fromIndex) {
if (this == null) {
throw new TypeError('"this" is null or not defined');
}
// 1. Let O be ? ToObject(this value).
var o = Object(this);
// 2. Let len be ? ToLength(? Get(O, "length")).
var len = o.length >>> 0;
// 3. If len is 0, return false.
if (len === 0) {
return false;
}
// 4. Let n be ? ToInteger(fromIndex).
// (If fromIndex is undefined, this step produces the value 0.)
var n = fromIndex | 0;
// 5. If n ≥ 0, then
// a. Let k be n.
// 6. Else n < 0,
// a. Let k be len + n.
// b. If k < 0, let k be 0.
var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);
function sameValueZero(x, y) {
return x === y || (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y));
}
// 7. Repeat, while k < len
while (k < len) {
// a. Let elementK be the result of ? Get(O, ! ToString(k)).
// b. If SameValueZero(searchElement, elementK) is true, return true.
if (sameValueZero(o[k], searchElement)) {
return true;
}
// c. Increase k by 1.
k++;
}
// 8. Return false
return false;
}
});
}
For array.prototype.flat
the MDN site also provides alternative solutions. One of which leverages array.prototype.reduce
and array.prototype.concat
:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat#Alternative
Browser support for array.includes and alternatives
Instead of using an API that is currently marked as "experimental" consider using a more broadly supported method, such as Array.prototype.indexOf()
(which is also supported by IE).
Instead of t.title.includes(string)
you could use t.title.indexOf(string) >= 0
You can also use Array.prototype.filter()
to get a new array of strings that meet a certain criteria, as in the example below.
var arr = ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"];document.getElementById("input").onkeyup = function() { document.getElementById("output").innerHTML = arrayContainsString(arr,this.value);}document.getElementById("header").innerHTML = JSON.stringify(arr);
function arrayContainsString(array, string) { var newArr = array.filter(function(el) { return el.indexOf(string) >= 0; }); return newArr.length > 0;}
<input id="input" type="text" /><br/><div>array contains text:<span id="output" /></div><div id="header"></div>
Related Topics
Node.Js: Read a Text File into an Array. (Each Line an Item in the Array.)
Capture Value Out of Query String with Regex
Using Multiple Mongodb Databases with Meteor.Js
How to Close a Dropdown on Click Outside
Arrow Function in Object Literal
Normalizing Mousewheel Speed Across Browsers
Difference and Intersection of Two Arrays Containing Objects
Safe Evaluation of Arithmetic Expressions in JavaScript
How to Return a JavaScript String from a Webassembly Function
JavaScript Can't Access Private Properties
How to Smoothly Scroll to an Element in Pure JavaScript
Sort an Array of Objects Based on Another Array of Ids
JavaScript Inheritance: Object.Create VS New