Reputation: 85
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
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)
);
};
Upvotes: 1