网站搜索

typescript - 使用类型推断过滤数组


我正在尝试正确推断函数的返回类型。下面是示例:

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);

filteredUsers 变量的推断类型是 User[]。我希望它是一个 Users 数组,其中属性 type 应该是 userTypes 变量的联合类型,所以在这种情况下,role 的类型应该是 UserType.PUBLISHER | UserType.VIEWER。我不知道如何实现这一点。

答案#1

因为你关心 User 的一个特定子类型,其中 type 属性已从 UserType 缩小,所以修改 User 使其在 type 属性上成为泛型,并仍然默认使用原始版本,将是最简单的方法:

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

另外,如果你希望 getUsersByTypes 返回某个更窄的 T 对应的 User<T>[],那么你确实需要确保传递给它的 types 数组的类型比 UserType[] 更窄:

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

这意味着你的 userTypes 变量可能需要更仔细地整理,例如通过使用 const 断言来请求更具体的类型:

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

好了,现在我们终于可以继续了。你需要让 getUsersByTypes 在类型 T 上成为通用类型,这个类型对应于 types 参数的元素,并且你需要返回一个 User<T>[],像这样:

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

请注意,我为两个参数都使用了只读数组类型;这是因为 readonly X[] 比 X[] 更宽泛(你可以将后者赋值给前者,但不能反过来),并且由于 const 断言会生成 readonly 数组,最好将参数类型扩展为 readonly 数组(除非你想修改输入数组,而你并不想)。

现在这将按照调用者的预期工作:

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

至于实现,让编译器验证它是类型安全的有点麻烦。

你本质上需要将过滤器回调标记为自定义类型保护函数,因为编译器无法推断这一点。否则,filter() 不会返回比输入更具体的类型。

而且,当在其上调用 includes() 时,你还需要将 types 数组从 readonly T[] 扩展为 readonly UserType[],因为 TS 不允许你向 includes() 传递更宽的输入,尽管这样做完全是安全的。参见 TypeScript 常量断言:如何使用 Array.prototype.includes?。

但一旦你做了这两件事,编译器就会高兴了:

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 链接