Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript - Unexpected error when using includes with a typed array

Tags:

typescript

With the following code we produce two arrays:

const PROVIDERS: {
  PROVIDER_1: 'PROVIDER_1';
  PROVIDER_2: 'PROVIDER_2';
  PROVIDER_3: 'PROVIDER_3';
} = {
  PROVIDER_1: 'PROVIDER_1',
  PROVIDER_2: 'PROVIDER_2',
  PROVIDER_3: 'PROVIDER_3',
};

const GOOD_PROVIDERS = [PROVIDERS.PROVIDER_1, PROVIDERS.PROVIDER_2];
const BAD_PROVIDERS = [PROVIDERS.PROVIDER_3];

The types are:

const GOOD_PROVIDERS: ("PROVIDER_1" | "PROVIDER_2")[]
const BAD_PROVIDERS: ("PROVIDER_3"")[]

Then, if I try to check if a value is included in one of those arrays I face several compilation errors that I don't know how to handle:

const provider: 'PROVIDER_1' | 'PROVIDER_2' | 'PROVIDER_3' = getProvider();
const isGood = GOOD_PROVIDERS.includes(provider);
const isBad = BAD_PROVIDERS.includes(provider);

The includes of GOOD_PROVIDERS throws this error

Argument of type '"PROVIDER_1" | "PROVIDER_2" | "PROVIDER_3"' is not assignable to parameter of type '"PROVIDER_1" | "PROVIDER_2"'. Type '"PROVIDER_3"' is not assignable to type '"PROVIDER_1" | "PROVIDER_2"'.ts(2345)

The includes of BAD_PROVIDERS throws this error

Argument of type '"PROVIDER_1" | "PROVIDER_2" | "PROVIDER_3"' is not assignable to parameter of type '"PROVIDER_3"'. Type '"PROVIDER_1"' is not assignable to type '"PROVIDER_3"'.ts(2345)

Why does Array.includes assume that the argument is already in the list? Is there any other way to check this?

like image 581
sebbab Avatar asked Apr 29 '19 15:04

sebbab


2 Answers

Why does Array.includes assume that the argument is already in the list?

The link in your comment already kind of answers that. The TL;DR is that adding typings to an array is similar to creating an abstract set of values, and going beyond this set is chosen to be a compiler error. Some might like it, some might hate it, but it is what it is (at least, for now).

Is there any other way to check this?

Yes.

Starting with TypeScript 1.6 you can use (x) => x is Type type guards. I find this feature (along with (x) => asserts x is Type assertion functions) very convenient to work with, extremely useful, and kind of like magic to be honest:

In the following example, I'm going to assume that you don't have control over the definition of the PROVIDERS object. If you do, go to the next example.

const PROVIDERS: {
  PROVIDER_1: 'PROVIDER_1';
  PROVIDER_2: 'PROVIDER_2';
  PROVIDER_3: 'PROVIDER_3';
} = {
  PROVIDER_1: 'PROVIDER_1',
  PROVIDER_2: 'PROVIDER_2',
  PROVIDER_3: 'PROVIDER_3',
};

type GoodProvider = typeof PROVIDERS.PROVIDER_1 | typeof PROVIDERS.PROVIDER_2;
type BadProvider = typeof PROVIDERS.PROVIDER_3;
type Provider = GoodProvider | BadProvider;

// Notice that types here are broadened, – otherwie this won't work
// The distinction between good vs bad providers has been moved to typings instead
const GOOD_PROVIDERS: Provider[] = [PROVIDERS.PROVIDER_1, PROVIDERS.PROVIDER_2];
const BAD_PROVIDERS: Provider[] = [PROVIDERS.PROVIDER_3];

function isGoodProvider(provider: Provider): provider is GoodProvider {
  return GOOD_PROVIDERS.includes(provider);
}

// You might not need both functions though; but if you do, here's the second one
function isBadProvider(provider: Provider): provider is BadProvider {
  return BAD_PROVIDERS.includes(provider);
}

Try it.

If you're in control of the PROVIDERS object, you're in luck, because you can do some major optimizations, – both textual (less code to write) and logical (no logic duplication).

Using as const, you can define source-of-truth objects, and infer all the necessary typings from them, – this is the approach I use all over, and it is great in its robustness:

const GOOD_PROVIDERS = {
  PROVIDER_1: 'PROVIDER_1',
  PROVIDER_2: 'PROVIDER_2',
} as const;

const BAD_PROVIDERS = {
  PROVIDER_3: 'PROVIDER_3',
} as const;

type GoodProvider = keyof typeof GOOD_PROVIDERS;
type BadProvider = keyof typeof BAD_PROVIDERS;
type Provider = GoodProvider | BadProvider;

function isGoodProvider(provider: Provider): provider is GoodProvider {
  return provider in GOOD_PROVIDERS;
}

function isBadProvider(provider: Provider): provider is BadProvider {
  return provider in BAD_PROVIDERS;
}

Try it.

like image 79
Dima Parzhitsky Avatar answered Nov 30 '22 23:11

Dima Parzhitsky


I changed for some() method:

array.some((arr) => arr === otherParameter)

In your case would be:

const isGood = GOOD_PROVIDERS.some((GOOD_PROVIDER) => GOOD_PROVIDER === provider);
like image 45
Pablo Cocciaglia Avatar answered Dec 01 '22 00:12

Pablo Cocciaglia