news 2026/9/30 1:51:44

TypeScript 类型挑战 59:用 GetOptional<T> 提取对象类型中的可选属性

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TypeScript 类型挑战 59:用 GetOptional<T> 提取对象类型中的可选属性
  • 示例工程

【免费下载链接】type-challenges

Collection of TypeScript type challenges with online judge

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

本篇技术指南围绕 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 }>>, ]

这里有两个值得注意的细节:

  1. 判定工具: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等特殊类型。

  2. 第二个用例是陷阱用例:{ foo: undefined, bar?: undefined }中,foo是必选属性且值类型为undefined,bar是可选属性且值类型为undefined。期望结果是{ bar?: undefined }。这个用例专门用来拦截"只看值类型"的朴素解法——因为foo与bar的值类型都是undefined,仅凭值类型根本无法区分谁是必选谁是可选。这逼迫解法必须在**键的可选性(modifier)**层面做文章。

思路分析:从问题分解到键集合筛选

GetOptional<T>的实现可以分解为三个步骤:

  1. 遍历所有键:使用映射类型{ [K in keyof T]: ... };
  2. 判断每个键是否为可选键:这是全题的核心难点,需要一种能在类型层面回答"K是否可选"的判定手段;
  3. 筛选:在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]:保留原属性值类型(包括其可选修饰符语义下的联合类型)。

验证两个测试用例:

  1. GetOptional<{ foo: number, bar?: string }>:{} extends { foo: number }为 false → 丢弃foo;{} extends { bar?: string }为 true → 保留bar,结果为{ bar?: string }✓;
  2. 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):

  1. 查看题目描述:questions/00059-hard-get-optional/README.ja.md(或 README.zh-CN.md、README.md);
  2. 打开模板文件 template.ts,其中初始实现为type GetOptional<T> = any,需替换为上文解法;
  3. 运行测试用例 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

项目地址:https://gitcode.com/GitHub_Trending/ty/type-challenges
点击查看免费下载
上一篇:tf_efficientnetv2_s.in21k_ft_in1k部署实战:从开发到生产环境的完整指南
下一篇:Agent-Native模板解析:AI辅助幻灯片设计的终极指南

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

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

AI 替你打开网页并总结内容:BrowserSkill 5 分钟上手指南

AI 替你打开网页并总结内容&#xff1a;BrowserSkill 5 分钟上手指南 【免费下载链接】BrowserSkill Let AI agents use your real, logged-in browser without interrupting your work. CLI extension for browser automation across any shell-capable AI agent. 项目地址…

作者头像 李华
网站建设 2026/9/30 1:51:37

三步把 Switch 变成客厅 B 站大屏:wiliwili 客户端安装与上手

三步把 Switch 变成客厅 B 站大屏&#xff1a;wiliwili 客户端安装与上手 【免费下载链接】wiliwili 第三方B站客户端&#xff0c;目前可以运行在PC全平台、PSVita、PS4 、Xbox 和 Nintendo Switch上 项目地址: https://gitcode.com/GitHub_Trending/wi/wiliwili wiliwi…

作者头像 李华
网站建设 2026/9/30 1:49:58

存储系统可观测性终极实践:OpenTelemetry、eBPF 与微秒级指标全景透视

存储系统可观测性终极实践&#xff1a;OpenTelemetry、eBPF 与微秒级指标全景透视在现代微服务、海量分布式与高并发存储交织的复杂拓扑中&#xff0c;当线上出现一个“某用户支付接口耗时从 2ms 突发涨到 200ms”的性能抖动时&#xff0c;传统的监控手段往往只能两眼一抹黑&am…

作者头像 李华