news 2026/10/2 13:48:03

type-challenges 实战:实现类型级数组过滤器 Filter<T, Predicate>(题目 18220)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
type-challenges 实战:实现类型级数组过滤器 Filter<T, Predicate>(题目 18220)
  • 示例工程

【免费下载链接】type-challenges

Collection of TypeScript type challenges with online judge

项目地址:https://gitcode.com/GitHub_Trending/ty/type-challenges
点击查看免费下载

导读

本文围绕 type-challenges 仓库中的中级题目18220 Filter展开,目标是用纯 TypeScript 类型系统实现一个「数组过滤器」Filter<T, Predicate>:给定一个数组类型T和一个由原始类型(或其联合)构成的谓词Predicate,返回仅保留「类型属于Predicate」的元素的数组类型。读完本文,你将掌握条件类型在元组上的递归拆分技巧、联合类型的匹配判定、以及如何利用仓库自带的测试框架(@type-challenges/utils)验证类型答案的正确性。

一、题目速览:规格、难度与标签

本题收录于仓库的 questions/18220-medium-filter 目录,官方描述如下:

实现类型Filter<T, Predicate>。其中T是数组,Predicate是原始类型(primitive type)或原始类型的联合(union)。结果应当是只包含「类型属于Predicate」的元素的数组。

配套的元数据位于 info.yml,明确给出了本题的定位:

difficulty: medium title: Filter tags: array, filter author: github: mu-hun name: Mu-Hun
  • 难度:medium(中等),适合已经熟悉条件类型、元组递归的读者挑战;
  • 标签:array、filter,说明本题考查的核心领域是数组(元组)类型的变换;
  • 作者:Mu-Hun(@mu-hun)。

题目本身可以在线挑战(README 中提供了 Judge 入口),也可以在本仓库本地完成并通过类型检查验证答案。

二、题目环境:模板、测试用例与工具包

2.1 起始模板

template.ts 给出了待填充的骨架,默认实现是一个空数组,显然需要完全重写:

type Filter<T extends any[], P> = []

约束条件T extends any[]表明入参T必须是数组/元组类型,P没有显式约束,理论上可以传入任意类型,但按题目本意应当是原始类型或其联合。

2.2 测试用例(题目判定的唯一标准)

test-cases.ts 是本题通过与否的判定依据:

import type { Equal, Expect } from '@type-challenges/utils' type Falsy = false | 0 | '' | null | undefined type cases = [ Expect<Equal<Filter<[0, 1, 2], 2>, [2]>>, Expect<Equal<Filter<[0, 1, 2], 0 | 1>, [0, 1]>>, Expect<Equal<Filter<[0, 1, 2], Falsy>, [0]>>, ]

三个用例分别覆盖三类场景:

  1. 单个原始类型谓词:Filter<[0, 1, 2], 2>应得到[2],即只保留等于2的元素;
  2. 联合类型谓词:Filter<[0, 1, 2], 0 | 1>应得到[0, 1],即保留所有属于联合0 | 1的元素;
  3. Falsy 联合谓词:Falsy = false | 0 | '' | null | undefined,Filter<[0, 1, 2], Falsy>应得到[0]。注意这里的0是Falsy的成员,因此0会被保留,而1、2被过滤掉。

2.3 测试工具包

Equal与Expect来自本仓库的 utils/index.d.ts(包名@type-challenges/utils,见 utils/package.json)。其中Equal使用了经典的函数签名比较法,能够区分any、never等「看似相等实则不同」的类型:

export type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false export type Expect<T extends true> = T

Expect<...>要求传入的类型严格等于true,一旦你的Filter结果与期望值不一致,整个cases类型声明就会报编译错误。这正是 type-challenges 一类题目的核心机制——用类型系统自己来判定类型答案的对错。

三、解题思路:元组递归 + 条件类型

数组过滤的本质是一个「遍历 + 筛选」过程。在类型层面,我们无法写循环,但可以借助条件类型对元组的递归拆分完成:

  1. 用T extends [infer F, ...infer Rest]把元组拆成「第一个元素F」和「剩余部分Rest」;
  2. 判断F是否属于谓词P;
  3. 属于则把F放回结果头部,并继续处理Rest;不属于则跳过F,直接处理Rest;
  4. 当T为空元组时递归终止,返回[]。

3.1 标准解

type Filter<T extends any[], P> = T extends [infer F, ...infer Rest] ? F extends P ? [F, ...Filter<Rest, P>] : Filter<Rest, P> : []

执行推演(以Filter<[0, 1, 2], 2>为例):

  • [0, 1, 2]拆出F = 0、Rest = [1, 2],0 extends 2为假 → 走Filter<[1, 2], 2>;
  • [1, 2]拆出F = 1,1 extends 2为假 → 走Filter<[2], 2>;
  • [2]拆出F = 2,2 extends 2为真 → 走[2, ...Filter<[], 2>];
  • []无法匹配拆分模式 → 返回[];
  • 最终结果[2],与用例期望一致。

3.2 借助内置工具类型Extract

条件类型F extends P ? ... : ...本质上就是在做集合(成员)判定,也可以显式地用内置的Extract<F, P>表达「取出 F 中属于 P 的部分」:

type Filter<T extends any[], P> = T extends [infer F, ...infer Rest] ? Extract<F, P> extends never ? Filter<Rest, P> : [F, ...Filter<Rest, P>] : []

这里Extract<F, P> extends never表示「F 与 P 没有交集」→ 过滤掉;否则保留。两种写法等价,标准解更简洁,Extract版语义更显式,适合作为思路二。

3.3 更严谨的写法:元组包裹避免分发

条件类型的左侧如果是裸类型参数(naked type parameter),会触发联合类型分发(distributive conditional types)。在上面的标准解中,F是从infer推断出来的具体类型,本身不会触发分发;但为了养成良好习惯、应对「元素本身是联合类型」的极端输入,可以用元组包裹来显式禁用分发:

type Filter<T extends any[], P> = T extends [infer F, ...infer Rest] ? [F] extends [P] ? [F, ...Filter<Rest, P>] : Filter<Rest, P> : []

对本题测试用例而言,三种写法的结果一致,均可通过验证。

四、易错点与边界分析

  1. 联合类型谓词不参与分发:F extends P中F是具体的推断类型、P是联合,因此F不会对P分发,0 extends 0 | 1会得到true,这正是用例 2 能通过的关键。如果你把写法改成P extends F,语义就会变成「P 整体包含于 F」,方向就反了。

  2. 0与 Falsy 的关系:0 extends (false | 0 | '' | null | undefined)为真,所以Filter<[0, 1, 2], Falsy>返回[0]。这提醒我们:题目中的谓词是类型层面的包含关系,与运行时Boolean(0) === false的「假值」概念不是一回事——类型过滤只看类型归属。

  3. never元素的处理差异:标准解中never extends P恒为真,即never元素会被「保留」。本题的测试用例没有涉及never,因此三种写法都能通过;但如果想剔除never,就需要单独处理,这正是 hard 难度题目 00399 FilterOut 的考点。

  4. boolean与false的差异:boolean类型等价于true | false,因此boolean extends false为假。若谓词只含false而元素是boolean,该元素不会被保留。这类边界在 00399-hard-tuple-filter/test-cases.ts 的用例中体现得更明显(例如FilterOut<[1, never, 'a', undefined, false, null], never | null | undefined>期望得到[1, 'a', false],其中false被保留、undefined/null/never被剔除)。

五、与 Hard 题目 FilterOut(00399)的对比

本仓库还有一道姊妹题 00399-hard-tuple-filter,要求实现FilterOut<T, F>,把「属于F」的元素全部剔除。两者正好互为镜像:

  • Filter(18220,medium):保留「属于谓词」的元素;
  • FilterOut(00399,hard):剔除「属于过滤集合」的元素。

FilterOut 的测试用例额外覆盖了never的剔除、[never, 1, 'a', undefined, false, null]与never | null | undefined的组合,以及「元素本身是联合类型」时[number | null | undefined, never]应整体保留的情况。从源码结构看,FilterOut 需要在 Filter 的思路上额外处理never与元素级联合的判定,难度因此更高。建议先完成 18220,再挑战 00399,形成「一正一反」的完整认知。

六、本地验证方法

type-challenges 仓库以 pnpm 作为包管理器(见根目录 package.json 中的"packageManager": "pnpm@8.12.1"),并在 devDependencies 中通过 workspace 引入@type-challenges/utils:

"@type-challenges/utils": "workspace:*"

本地验证答案的典型步骤:

  1. 在仓库根目录执行pnpm install,安装工作区依赖(含@type-challenges/utils与 TypeScript 5.3.x,见 utils/package.json);
  2. 将答案写入 questions/18220-medium-filter/template.ts 的Filter;
  3. 对测试用例做类型检查,例如:
npx tsc --noEmit questions/18220-medium-filter/test-cases.ts

当Filter实现正确时,cases中的每个Expect<Equal<...>>都展开为true,编译无错误;实现不正确时,编译器会指出Expect的约束被违反。仓库根目录的 tsconfig.base.json 开启了strict等严格选项,能帮助捕捉更多类型层面的细节。

七、小结

Filter<T, Predicate>是 type-challenges 中非常经典的 medium 级数组题,核心公式可概括为:

  • 递归拆分:T extends [infer F, ...infer Rest];
  • 条件筛选:F extends P ? 保留 : 跳过;
  • 终止条件:空元组返回[]。

在此基础上,你可以继续延伸:加入never处理即为 hard 题 FilterOut(00399),还可以尝试与仓库中其他数组类题目(如 Flatten、Reverse)对比,体会「元组递归 + 条件类型」这一通用范式在类型体操中的复用价值。文中给出的三种实现写法均可通过 test-cases.ts 的全部断言,读者可根据可读性偏好自行选用。

  • 示例工程

【免费下载链接】type-challenges

Collection of TypeScript type challenges with online judge

项目地址:https://gitcode.com/GitHub_Trending/ty/type-challenges
点击查看免费下载
上一篇:Serial Studio 模块化核心重构:基于 core/ 分层静态库与消息总线的架构治理实战
下一篇:react-use 的 useFirstMountState:精准识别组件首次渲染的 Hooks 实现与实战

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 13:47:54

拼团系统人群标签设计实战:从规则到算法,驱动用户增长与精准营销

拼团这种玩法&#xff0c;本质上是在用社交关系换流量红利。一套拼团系统上线容易&#xff0c;真正决定它能跑多远、转化率能拉到多高的&#xff0c;往往是底层那套看不见的“人群标签”体系。我做过几个电商和社区团购方向的项目&#xff0c;每次复盘时都发现&#xff0c;凡是…

作者头像 李华
网站建设 2026/10/2 13:47:31

智慧商城整体解决方案:从数据流到落地的工程实践

简介&#xff1a;这份《智慧商城整体解决方案.ppt》面向电商运营、微商操盘手及企业市场人员&#xff0c;系统梳理了从品牌展示到客户沉淀的完整线上商业闭环。内容围绕微网站与微场景搭建、砸金蛋与幸运大转盘等营销插件、全民经纪人与微助力等线上推广玩法展开&#xff0c;并…

作者头像 李华
网站建设 2026/10/2 13:45:14

【C语言】整型数组(Finish)

分静态数组&#xff08;栈&#xff09;、动态数组&#xff08;堆&#xff0c;malloc&#xff09;两种。1. 静态数组&#xff08;固定大小&#xff0c;编译时确定长度&#xff09;方式1&#xff1a;直接定义 // 定义长度为5的int数组&#xff0c;5个元素&#xff1a;a[0],a[1],a…

作者头像 李华
网站建设 2026/10/2 13:43:44

VS Code 插件及快捷键配置:用 TaoToken 统一 Key 打通 AI 编程工作流

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 13:41:13

Mixture of Experts in Large Language Models

文章主要内容总结 本文是对混合专家(Mixture-of-Experts, MoE)架构在大型语言模型(LLM)中的全面综述,核心内容涵盖: 理论基础与发展历程:追溯MoE从早期自适应学习系统到现代大规模稀疏激活架构的演变,重点梳理2020年后GShard、Switch Transformer等里程碑模型推动MoE成…

作者头像 李华