PAYATHON 2026

TypeScript 如何将任意字符串列表安全过滤为白名单类型?

数据阿明

可以避免这些类型断言。不要求保留 URL 参数键的顺序时,可以遍历白名单,用 Set<string> 检查键是否存在。筛选结果会保留白名单的元素类型。

需要保留 Object.keys(filters) 的顺序时,可以先把白名单赋给 readonly string[] 变量,再用类型谓词筛选,也不需要 as string[]。

方法一:遍历白名单,用 Set 判断是否存在

假设 PageFilterId 是字符串联合类型,WhiteListFilterIds 的类型是 PageFilterId[] 或 readonly PageFilterId[]:

const filterKeys = new Set<string>(Object.keys(filters));

const activeFilters = WhiteListFilterIds.filter(id =>
  filterKeys.has(id)
);
// activeFilters: PageFilterId[]

这里的类型可以直接配合:

  • filterKeys.has 接受任意 string,因此也接受 PageFilterId。
  • 被筛选的数组本身包含 PageFilterId,所以结果不需要额外断言。

筛选结果按白名单的顺序排列。如果白名单有重复项,结果也可能重复。原来的写法按 Object.keys(filters) 的顺序排列,对象键不会重复。能否替换,取决于你需要哪种行为。

如果白名单声明为普通的 string[],结果也只会是 string[]。要保留更具体的类型,应在定义白名单时准确声明其元素类型。

为什么 includes 不接受 string?

对于元素类型为 T 的数组,includes 的参数约束可以概括为:

includes(searchElement: T, fromIndex?: number): boolean;

白名单的元素类型是 PageFilterId,includes 的参数就需要是 PageFilterId。而 Object.keys(filters) 返回的是 string[],其中的字符串可能不属于这个联合类型,所以直接传入会报错。

运行时可以检查其他字符串,但当前类型签名没有区分允许检查的值和数组元素的类型。此外,includes 返回的是 boolean,没有声明类型谓词,因此不能直接用来收窄类型。

方法二:保留原顺序,使用只读视图和类型谓词

可以通过普通赋值拓宽读取白名单时使用的类型:

const whitelistValues: readonly string[] = WhiteListFilterIds;

const activeFilters = Object.keys(filters).filter(
  (f): f is PageFilterId => whitelistValues.includes(f)
);
// activeFilters: PageFilterId[]

PageFilterId 属于 string,因此可以把它的数组赋给 readonly string[],无需类型断言。赋值不会复制或转换数据,whitelistValues 仍然引用原数组。

readonly 可以避免通过这个较宽的类型视图向白名单写入任意字符串。它只限制该引用上的写操作,不会冻结原数组。

类型谓词仍然需要正确实现

类型谓词是开发者提供的类型承诺,TypeScript 不会证明函数体真的满足它。这里能把命中的字符串视为 PageFilterId,是因为白名单中的每个成员都具有 PageFilterId 类型。

因此,白名单的定义应受到类型检查,例如:

type PageFilterId = "status" | "sort";

const WhiteListFilterIds: readonly PageFilterId[] = [
  "status",
  "sort",
];

如果白名单只覆盖 PageFilterId 的一部分,未命中只能说明值不在当前白名单中,不能说明它一定不属于整个 PageFilterId 联合类型。将判断提取成通用类型守卫时,尤其要考虑这一点;上面的 filter 用法只保留命中的值。

允许按白名单排序时,第一种写法更直接;需要保持原键顺序时,第二种写法更贴近现有代码。

备注:内容仅供参考。