- 示例工程
【免费下载链接】type-challenges
Collection of TypeScript type challenges with online judge
本篇技术指南围绕 type-challenges 仓库第 59 号挑战「Get Optional」展开,完整讲解高级工具类型GetOptional<T>的题目要求、测试用例语义、实现思路与最终解法,并对照第 57 号挑战GetRequired<T>进行差分分析。读完本文后,你将掌握如何用映射类型(Mapped Types)与as子句在类型层面筛选键集合,并理解"可选属性"在类型系统中的精确判定方式(包括undefined值属性的边界陷阱),能够独立实现这一类"按键属性筛选"的工具类型。
题目概览:难度、标签与作者
本挑战位于 questions/00059-hard-get-optional/,由作者 Zheeeng 提出,属于hard(困难)级别。从其元信息文件 info.yml 可以看到:
difficulty: hard:困难难度;title: Get Optional;tags: utils, infer:标签为"工具类型"与"类型推断";related: 57:关联第 57 号挑战 Get Required。
题目在日文版 README.ja.md 中描述为:实现一个高级实用工具类型GetOptional<T>,该类型仅保留所有**可选(optional)**字段。配套的简体中文版 README.zh-CN.md 给出了同样的描述:"实现高级工具类型GetOptional<T>,该类型保留所有可选属性"。
目标与示例
题目给出的最小示例非常直观:
type I = GetOptional<{ foo: number, bar?: string }> // expected to be { bar?: string }即:输入一个对象类型,foo是必选属性、bar是带?的可选属性;输出应只保留bar?: string,把必选属性foo剔除掉。
在动手之前,我们先理解"可选"在类型层面的含义:bar?: string表示该属性可以缺省,其实际取值类型在读取时是string | undefined。因此GetOptional<T>本质上要做的是——遍历keyof T,判断每个键是否为可选键,然后只保留那些可选键对应的属性。
测试用例深度解读:为什么要设计第二个用例
题目自带的测试文件 test-cases.ts 定义了判定标准:
import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<GetOptional<{ foo: number, bar?: string }>, { bar?: string }>>, Expect<Equal<GetOptional<{ foo: undefined, bar?: undefined }>, { bar?: undefined }>>, ]这里有两个值得注意的细节:
判定工具:
Equal与Expect来自仓库独立的工具包@type-challenges/utils,其实现位于 utils/index.d.ts。其中Equal<X, Y>使用函数参数逆变位置的类型等价性判定((<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2)),比extends单向可赋值判定更严格,能精确区分any、unknown等特殊类型。第二个用例是陷阱用例:
{ foo: undefined, bar?: undefined }中,foo是必选属性且值类型为undefined,bar是可选属性且值类型为undefined。期望结果是{ bar?: undefined }。这个用例专门用来拦截"只看值类型"的朴素解法——因为foo与bar的值类型都是undefined,仅凭值类型根本无法区分谁是必选谁是可选。这逼迫解法必须在**键的可选性(modifier)**层面做文章。
思路分析:从问题分解到键集合筛选
GetOptional<T>的实现可以分解为三个步骤:
- 遍历所有键:使用映射类型
{ [K in keyof T]: ... }; - 判断每个键是否为可选键:这是全题的核心难点,需要一种能在类型层面回答"
K是否可选"的判定手段; - 筛选:在
as子句中把非可选键映射为never,实现键集合的过滤。
as子句是 TS 4.1+ 提供的能力,形如{ [K in keyof T as NewK]: T[K] },其中NewK可以是never——一旦某个键被映射为never,该属性就不会出现在结果类型中。
那么"如何判断一个键是否可选"呢?一个自然的思路是利用内置的Required<T>:Required<T>会把 T 的所有属性都变成必选(去掉?)。于是对每个键K:
- 若
K原本必选,则T[K]与Required<T>[K]完全一致; - 若
K原本可选,则T[K]是X | undefined而Required<T>[K]是X。
但这会踩中前面提到的陷阱:当可选属性的值类型本身就是undefined时(如bar?: undefined),T['bar']与Required<T>['bar']都是undefined,二者无法区分。
关键难点:{} extends Pick<T, K>判定法
更稳健的方案是利用"空对象是否能赋值给该键的对象类型"这一特性。构造辅助类型:
type IsOptionalKey<T, K extends keyof T> = {} extends Pick<T, K> ? true : false原理如下:
- 如果
K是可选属性,那么Pick<T, K>形如{ bar?: string },所有属性都可缺省,因此空对象{}可以赋值给它(满足结构兼容),{} extends Pick<T, K>成立; - 如果
K是必选属性,Pick<T, K>形如{ foo: number }或{ foo: undefined },空对象{}缺少该必选属性,无法赋值,判定为false。
关键在于:这个判定发生在"属性是否存在"的层面,而不是"值类型是什么"的层面,所以无论可选属性的值类型是string还是undefined,判定结果都正确。这正是它能通过{ foo: undefined, bar?: undefined }用例的原因。
参考解法:完整实现
基于上述思路,一个简洁且能通过全部测试用例的实现如下:
type GetOptional<T> = { [K in keyof T as {} extends Pick<T, K> ? K : never]: T[K] }逐段拆解:
keyof T:取出 T 的所有键;Pick<T, K>:提取单个键K对应的属性,得到{ K: T[K] }或{ K?: T[K] };{} extends Pick<T, K> ? K : never:若K为可选键则保留为K,否则映射为never将其丢弃;: T[K]:保留原属性值类型(包括其可选修饰符语义下的联合类型)。
验证两个测试用例:
GetOptional<{ foo: number, bar?: string }>:{} extends { foo: number }为 false → 丢弃foo;{} extends { bar?: string }为 true → 保留bar,结果为{ bar?: string }✓;GetOptional<{ foo: undefined, bar?: undefined }>:{} extends { foo: undefined }为 false → 丢弃foo;{} extends { bar?: undefined }为 true → 保留bar,结果为{ bar?: undefined }✓。
与之配套,第 57 号挑战GetRequired<T>只需把条件取反,实现几乎是对偶的:
type GetRequired<T> = { [K in keyof T as {} extends Pick<T, K> ? never : K]: T[K] }其测试用例 questions/00057-hard-get-required/test-cases.ts 同样包含Expect<Equal<GetRequired<{ foo: undefined, bar?: undefined }>, { foo: undefined }>>——两个挑战互为镜像,验证了同一套判定逻辑在"保留必选"与"保留可选"两个方向上都成立。
在仓库中运行与验证
要亲身体验这道题,可以按如下方式操作(仓库根目录为GitHub_Trending/ty/type-challenges):
- 查看题目描述:questions/00059-hard-get-optional/README.ja.md(或 README.zh-CN.md、README.md);
- 打开模板文件 template.ts,其中初始实现为
type GetOptional<T> = any,需替换为上文解法; - 运行测试用例 test-cases.ts,确保两个
Expect<Equal<...>>全部通过。仓库根 package.json 采用 pnpm workspace 管理,@type-challenges/utils为 workspace 依赖(见 utils/package.json),TypeScript 版本为 ^5.3.3,tsconfig基于 tsconfig.base.json(strict: true),可在编辑器或tsc --noEmit下获得即时的类型检查反馈。
延伸思考
- 判定方式的可替换性:
{} extends Pick<T, K>之外,社区也常见T[K] extends Required<T>[K]的写法,后者对"值类型为undefined的可选属性"会失效,这正是本题测试用例刻意设计的区分点; - 与
keyof家族的组合:GetOptional/GetRequired与仓库中其他键筛选类挑战(如 00005-extreme-readonly-keys 判断 readonly 键)共享同一套"映射类型 + as never + 条件判定"的方法论,掌握后可以快速迁移到任意属性修饰符筛选场景; - 实际工程价值:在编写表单校验、配置合并、可选配置默认值填充等场景中,"区分对象类型里哪些键可选、哪些键必选"是常见的类型层需求,
GetOptional这类工具可以直接支撑Partial/Required之外更精细的类型变换。
- 示例工程
【免费下载链接】type-challenges
Collection of TypeScript type challenges with online judge
相关推荐
CANN/GE模型执行API文档
aclmdlExecute<a name="ZH CN_TOPIC_0000001264921926" </a 产品支持情况<a name="section15
人工智能深度学习模型编译模型优化编译器AscendHydra 命令行参数完全指南:理解 Hydra 专属 Flags 与配置覆盖机制
Hydra 命令行参数完全指南:理解 Hydra 专属 Flags 与配置覆盖机制 本篇技术指南以 Hydra 1.0 官方文档《Hydra's command
示例工程CloudNativePG 资源管理深度指南:从 QoS 调度到 VPA/HPA 自动伸缩实践
CloudNativePG 资源管理深度指南:从 QoS 调度到 VPA/HPA 自动伸缩实践 导读 本文围绕 CloudNativePG(CNPG)官方文档中
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考