Antek
Antek

Reputation: 85

Filter array with type inference

I'm trying to properly infer function result type. Example below:

enum UserType {
  ADMIN,
  VIEWER,
  PUBLISHER,
}

interface User {
  name: string;
  type: UserType;
}

const userTypes = [UserType.PUBLISHER, UserType.VIEWER];

const users = [
  { name: 'Paul', type: UserType.ADMIN },
  { name: 'Steve', type: UserType.VIEWER },
  { name: 'Marry', type: UserType.PUBLISHER },
];

const getUsersByTypes = (users: User[], types: UserType[]) => {
  return users.filter(user => types.includes(user.type));
};

const filteredUsers = getUsersByTypes(users, userTypes);

Inferred type of filteredUsers variable is User[]. I wish it to be an array of Users where property type should be an union of userTypes variable, so in this case role should be type of UserType.PUBLISHER | UserType.VIEWER. Can't figure out how to achive that.

Upvotes: 2

Views: 257

Answers (1)

jcalz
jcalz

Reputation: 327849

Because you care about a specific subtype of User where the type property has been narrowed from UserType, it will be easiest to modify User to be generic in the type property, while still having it default to the original version:

interface User<T extends UserType = UserType> {
  name: string;
  type: T;
}

Also, if you want getUsersByTypes to return User<T>[] for some narrower T, then you really need to make sure that the types array passed to it has a type narrower than UserType[]:

const userTypes = [UserType.PUBLISHER, UserType.VIEWER];
// const userTypes: UserType[] // 👎

That means your userTypes variable probably needs to be curated a little more closely, say by using a const assertion to ask for a more specific type:

const userTypes = [UserType.PUBLISHER, UserType.VIEWER] as const;
// const userTypes: readonly [UserType.PUBLISHER, UserType.VIEWER] 👍

Okay, now we're finally in a place to proceed. You're going to want to make getUsersByTypes generic in the type T corresponding to the elements of the types argument, and you're going to want to return a User<T>[], like this:

declare const getUsersByTypes: <T extends UserType>(
  users: readonly User[], types: readonly T[]
) => User<T>[]

Note that I used a readonly array type for both parameters; that's because readonly X[] is wider than X[] (you can assign the latter to the former but not vice versa) and since const assertions result in readonly arrays, it's best to widen to readonly array parameters (unless you want to modify the input arrays, which you don't).

That will now work as desired from the caller's side:

const filteredUsers = getUsersByTypes(users, userTypes);
// const filteredUsers: User<UserType.VIEWER | UserType.PUBLISHER>[]

As for the implementation, it's a bit of a hassle to try to get the compiler to verify it as type safe.

You essentially need to mark the filter callback as a custom type guard function, since the compiler can't infer that. And otherwise filter() wouldn't return a narrower type than the input.

And you also need to widen the types array from readonly T[] to readonly UserType[] when calling includes() on it because TS doesn't like to let you pass a wider input to includes(), even though it's perfectly safe to do so. See TypeScript const assertions: how to use Array.prototype.includes? .

But once you do both of those the compiler is happy:

const getUsersByTypes = <T extends UserType>(
  users: readonly User[], types: readonly T[]
) => {
  return users.filter((user): user is User<T> => 
    (types as readonly UserType[]).includes(user.type)
  );
};

Playground link to code

Upvotes: 1

Related Questions