Set param type to one of multiple constant arrays and filter it

Viewed 32

The first param is a list of allowed strings that the user can use. It works fine.

The second param is an array of values, but it must be one of the multiple arrays of constants defined in the type. It works fine until I call methods like includes or indexOf on it. When I do it tells me that field is of type never and I want to understand why and how to make it work properly.

The commented code works fine (kind of a manual job of includes or indexOf), but it's not ideal and makes me even more confused on why the original doesn't work.

My code:

type AvailableConfigFields = ("a" | "b" | "c" | "more fields")[];
type AvailableProviderFields = ["a"] | ["b"] | ["c"];

function filterFieldsForProvider(
  requestedFields: AvailableConfigFields,
  providerFields: AvailableProviderFields
) {
  // When calling `.includes(field)` TS throws the error below:
  // Argument of type 'string' is not assignable to parameter of type 'never'.
  // Type 'string' is not assignable to type 'never'.
  return requestedFields.filter((field) => providerFields.includes(field));

  // The code below works fine, but obviously I don't want to do that

  // return requestedFields.filter((field) => {
  //   let isThere = false;
  //   providerFields.forEach((value) => {
  //     if (value === field) isThere = true;
  //   });
  //   return isThere;
  // });
}

1 Answers

This seems to be due to some issue with how TypeScript types Array.prototype.includes. For an array whose type is a union of non-overlapping constant tuples, the searchElement parameter's type is inferred as never.

However, if you define your AvailableProviderFields type instead as a single Tuple where the element inside is a union of constant types, then TypeScript seems to correctly figure out how to let you use Array.prototype.includes.

type AvailableConfigFields = ("a" | "b" | "c" | "more fields")[];
type AvailableProviderFields = ["a" | "b" | "c"];

function filterFieldsForProvider(
  requestedFields: AvailableConfigFields,
  providerFields: AvailableProviderFields
) {
  // Argument of type '"a" | "b" | "c" | "more fields"' is not assignable to parameter of type '"a" | "b" | "c"'.
  return requestedFields.filter(
    (field) => (providerFields as string[]).includes(field)
  );
}

TypeScript playground

However, you will still run into the problem of the type "a" | "b" | "c" | "more fields" not being able to be assigned to the type "a" | "b" | "c". I'm not sure why you've set these types up to be different here, but if the same strings were available in each that would resolve this problem.

If one set does need to be wider than the other, as in your example, then you may instead find it useful to use a type assertion to widen the type of providerFields to string[] so TypeScript has no problem checking to see if it includes any given string:

type AvailableConfigFields = ("a" | "b" | "c" | "more fields")[];
type AvailableProviderFields = ["a" | "b" | "c"];

function filterFieldsForProvider(
  requestedFields: AvailableConfigFields,
  providerFields: AvailableProviderFields
) {
  return requestedFields.filter(
    (field) => (providerFields as string[]).includes(field)
  );
}

TypeScript playground

Related