27

I honestly don't know if something is wrong with my settings or of if this is a typescript feature. In the following example:

type AllowedChars = 'x' | 'y' | 'z';
const exampleArr: AllowedChars[] = ['x', 'y', 'z'];

function checkKey(e: KeyboardEvent) { 
    if (exampleArr.includes(e.key)) { // <-- here
        // ...
    } 
}

The typescript compiler complains that Argument of type 'string' is not assignable to parameter of type 'AllowedChars'. But where am I assigning? Array.prototype.includes returns a boolean (which I am not storing). I could silence the error by a type assertion, like this:

if (exampleArr.includes(e.key as AllowedChars)) {}

But how is that correct, I am expecing user input which could be anything. I don't understand why a function (Array.prototype.includes()) made to check if an element is found in an array, should have knowledge about the type of input to expect.

My tsconfig.json (typescript v3.1.3):

 {
    "compilerOptions": {
      "target": "esnext",
      "moduleResolution": "node",
      "allowJs": true,
      "noEmit": true,
      "strict": true,
      "isolatedModules": true,
      "esModuleInterop": true,
      "jsx": "preserve",
    },
    "include": [
      "src"
    ],
    "exclude": [
      "node_modules",
      "**/__tests__/**"
    ]
  }

Any help would be appreciated!

2
  • 1
    By calling the function .includes(), you're effectively assigning e.key to the parameter. You declared the array as being an array of the AllowedChars type, and e.key is not of that type. Commented Oct 28, 2018 at 16:50
  • 1
    Just use exampleArr as string[]? Commented Oct 28, 2018 at 19:00

3 Answers 3

39

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("oops"); // actually writing to an AllowedChars[]

But since you're just reading from the array it's perfectly safe, and why readonly is recommended.


Playground link to code

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

8 Comments

Thank you for your elaborate answer!
That's a really nice use of conditional types. However, if you want narrowing, it raises an error and you have to use a this type annotation instead. In Playground
If you don't like being forced to cast the array to convince the typechecker you know what you're doing, you can go give a thumbs-up to this issue to show an interest in a mechanism that would allow these methods to be defined more accurately.
Excellent and well explained answer!
@jcalz Must be my mistake, commenting on the wrong answer (there are two questions answering this issue with multiple answers having similar but not-identical solutions).
|
1

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.

1 Comment

"You have to "cast" the string you are receiving to AllowedChars" 👍
1

Eventually just using .some instead of .includes did the trick for me. Here's the playground.

const FRUITS = ["apple", "banana", "orange"] as const
type Fruit = typeof FRUITS[number]

const isFruit = (value: string): value is Fruit =>
    FRUITS.some(fruit => fruit === value)

Comments

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.