type-challenges(ts类型体操): 11 - 元组转换为对象

作者:fxss日期:2026/1/30

11 - 元组转换为对象

by sinoon (@sinoon) #简单 #object-keys

题目

将一个元组类型转换为对象类型,这个对象类型的键/值和元组中的元素对应。

例如:

1const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const
2
3type result = TupleToObject<typeof tuple> // expected { 'tesla': 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}
4

在 Github 上查看:tsch.js.org/11/zh-CN

代码

1/* _____________ 你的代码 _____________ */
2
3type TupleToObject<T extends readonly PropertyKey[]> = {
4  [P in T[number]]: P
5}
6
7

关键解释:

  • type PropertyKey = string | number | symbol
  • T extends readonly PropertyKey[] 用于限制 T 必须是一个只读的属性键元组。
  • [P in T[number]] 用于遍历元组中的每个元素,将其作为对象的键。
  • P 是元组中的元素类型,通过 T[number] 来获取。

相关知识点

extends

使用维度核心作用示例场景
类型维度做类型约束或条件判断(类型编程核心)限定泛型范围、判断类型是否兼容、提取类型片段
语法维度做继承(复用已有结构)接口继承、类继承
extends 做类型约束或条件判断
  1. 泛型约束:限定泛型的取值范围
1// 约束 T 必须是「拥有 length 属性」的类型(比如 string/数组)
2function getLength<T extends { length: number }>(arg: T): number {
3  return arg.length;
4}
5
6// 合法调用(符合约束)
7getLength("hello"); //  string  length,返回 5
8getLength([1, 2, 3]); //  数组有 length,返回 3
9
10// 非法调用(超出约束)
11getLength(123); //  报错:number 没有 length 属性
12
  1. 条件类型:类型版 三元运算符
1// 基础示例:判断类型是否为字符串
2type IsString<T> = T extends string ? true : false;
3
4type A = IsString<"test">; // true(符合)
5type B = IsString<123>; // false(不符合)
6

分布式条件类型(联合类型专用): 当 T 是联合类型时,extends 会自动拆分联合类型的每个成员,逐个判断后再合并结果。

1type Union = string | number | boolean;
2
3// 拆分逻辑:string→string,number→never,boolean→never  合并为 string
4type OnlyString<T> = T extends string ? T : never;
5type Result = OnlyString<Union>; // Result = string
6

注意:只有泛型参数是 裸类型(没有被 []/{} 包裹)时,才会触发分布式判断:

1// 包裹后不触发分布式,整体判断 [string|number] 是否兼容 [string]
2type NoDist<T> = [T] extends [string] ? T : never;
3type Result2 = NoDist<Union>; // never(整体不兼容)
4
  1. 配合 infer:提取类型片段(黄金组合)
1// 提取 Promise 的返回值类型
2type UnwrapPromise<T> = T extends Promise<infer V> ? V : T;
3
4type C = UnwrapPromise<Promise<string>>; // string(提取成功)
5type D = UnwrapPromise<number>; // number(不满足条件,返回原类型)
6
extends 做继承(复用已有结构)
  1. 接口继承:复用 + 扩展属性
1// 基础接口
2interface User {
3  id: number;
4  name: string;
5}
6
7// 继承 User,并扩展新属性
8interface Admin extends User {
9  role: "admin" | "super_admin"; // 新增权限属性
10}
11
12// 必须包含继承的 + 扩展的所有属性
13const admin: Admin = {
14  id: 1,
15  name: "张三",
16  role: "admin"
17};
18
19// 多接口继承
20interface HasAge { age: number; }
21interface Student extends User, HasAge {
22  className: string; // 同时继承 User + HasAge
23}
24
  1. 类继承:复用父类的属性 / 方法
1class Parent {
2  name: string;
3  constructor(name: string) {
4    this.name = name;
5  }
6  sayHi() {
7    console.log(`Hi, ${this.name}`);
8  }
9}
10
11// 继承 Parent 
12class Child extends Parent {
13  age: number;
14  constructor(name: string, age: number) {
15    super(name); // 必须调用父类构造函数(初始化父类属性)
16    this.age = age;
17  }
18  // 重写父类方法
19  sayHi() {
20    super.sayHi(); // 调用父类原方法
21    console.log(`I'm ${this.age} years old`);
22  }
23}
24
25const child = new Child("李四", 10);
26child.sayHi(); // 输出:Hi, 李四  I'm 10 years old
27

补充:类实现接口用 implements(不是 extends

1// 定义接口(契约:规定必须有 id、name 属性,以及 greet 方法)
2interface Person {
3  id: number;
4  name: string;
5  greet(): void; // 仅定义方法签名,无实现
6}
7
8// 类实现接口(必须严格遵守契约)
9class Employee implements Person {
10  // 必须实现接口的所有属性
11  id: number;
12  name: string;
13
14  // 构造函数初始化属性
15  constructor(id: number, name: string) {
16    this.id = id;
17    this.name = name;
18  }
19
20  // 必须实现接口的 greet 方法(具体实现由类自己定义)
21  greet() {
22    console.log([`Hi, I'm ${this.name}, ID: ${this.id}`](https://xplanc.org/primers/document/zh/10.Bash/90.%E5%B8%AE%E5%8A%A9%E6%89%8B%E5%86%8C/EX.id.md));
23  }
24}
25
26// 实例化使用
27const emp = new Employee(1, "张三");
28emp.greet(); // 输出:Hi, I'm 张三, ID: 1
29
30
31// 接口1:基础信息
32interface Identifiable {
33  id: number;
34  getId(): number;
35}
36
37// 接口2:可打印
38interface Printable {
39  printInfo(): void;
40}
41
42// 类同时实现两个接口(必须实现所有接口的成员)
43class Product implements Identifiable, Printable {
44  id: number;
45  name: string; // 类可扩展接口外的属性
46
47  constructor(id: number, name: string) {
48    this.id = id;
49    this.name = name;
50  }
51
52  // 实现 Identifiable 的方法
53  getId(): number {
54    return this.id;
55  }
56
57  // 实现 Printable 的方法
58  printInfo() {
59    console.log(`Product: ${this.name}, ID: ${this.getId()}`);
60  }
61}
62
63const product = new Product(100, "手机");
64console.log(product.getId()); // 100
65product.printInfo(); // Product: 手机, ID: 100
66

readonly

  • 核心作用:标记后,目标(属性 / 数组 / 元组)只能在初始化阶段赋值(比如接口实例化、类构造函数、变量声明时),后续任何修改运算都会被 TS 编译器拦截报错;
  • 运行时特性:readonly 仅做编译时检查,不会生成任何额外 JS 代码,也无法真正阻止运行时的修改(比如通过类型断言绕开的话,运行时仍能改);
  • const 的区别:const 是变量层面的不可重新赋值(但变量指向的对象 / 数组内部属性仍可改),readonly 是属性 / 类型层面的不可修改(变量本身可重新赋值,除非变量也用 const)。

常用使用场景:

  1. 作用于接口 / 类型别名的属性(最基础)
1// 定义带只读属性的接口
2interface User {
3  readonly id: number; // 只读属性:只能初始化赋值,后续不可改
4  name: string; // 普通属性:可修改
5}
6
7// 初始化时赋值(合法)
8const user: User = { id: 1, name: "张三" };
9
10// 尝试修改只读属性(报错)
11user.id = 2; //  报错:无法分配到 "id",因为它是只读属性
12// 修改普通属性(合法)
13user.name = "李四"; //  合法
14
  1. 作用于类的属性: 类中使用 readonly 标记属性,只能在声明时构造函数中赋值,后续无法修改
1class Person {
2  readonly id: number; // 只读属性
3  name: string;
4
5  // 构造函数中给 readonly 属性赋值(唯一合法的后续赋值方式)
6  constructor(id: number, name: string) {
7    this.id = id;
8    this.name = name;
9  }
10
11  updateInfo() {
12    this.id = 100; //  报错:id 是只读属性
13    this.name = "王五"; //  合法
14  }
15}
16
17const person = new Person(1, "赵六");
18person.id = 2; //  报错:只读属性不可修改
19
  1. 作用于数组 / 元组(只读数组): readonly 可标记数组为 “只读数组”,禁止修改数组元素、调用 push/pop 等修改方法
1// 方式1:使用 readonly 修饰数组类型
2const arr1: readonly number[] = [1, 2, 3];
3arr1.push(4); //  报错:readonly 数组不存在 push 方法
4arr1[0] = 10; //  报错:无法修改只读数组的元素
5
6// 方式2:使用 ReadonlyArray<T> 类型(等价于 readonly T[])
7const arr2: ReadonlyArray<string> = ["a", "b"];
8arr2.pop(); //  报错
9
10// 作用于元组(只读元组)
11type Point = readonly [number, number];
12const point: Point = [10, 20];
13point[0] = 30; //  报错:只读元组元素不可修改
14
  1. 结合 keyof + in 批量创建只读类型(映射类型)
1interface Product {
2  name: string;
3  price: number;
4  stock: number;
5}
6
7// 批量创建只读版本的 Product(TS 内置的 Readonly<T> 就是这么实现的)
8type ReadonlyProduct = {
9  readonly [K in keyof Product]: Product[K];
10};
11
12const product: ReadonlyProduct = { name: "手机", price: 2999, stock: 100 };
13product.price = 3999; //  报错:price 是只读属性
14
15// TS 内置了 Readonly<T>,可直接使用(无需手动写映射类型)
16const product2: Readonly<Product> = { name: "电脑", price: 5999, stock: 50 };
17product2.stock = 60; //  报错
18
  1. 只读索引签名:如果类型使用索引签名,也可以标记为 readonly,禁止通过索引修改属性
1// 只读索引签名:只能读取,不能修改
2type ReadonlyDict = {
3  readonly [key: string]: number;
4};
5
6const dict: ReadonlyDict = { a: 1, b: 2 };
7dict["a"] = 3; //  报错:索引签名是只读的
8console.log(dict["b"]); //  合法:仅读取
9

in

in 运算符用于遍历联合类型中的每个成员,将其转换为映射类型的属性名。

例如:

1interface Todo {
2  title: string
3  description: string
4  completed: boolean
5}
6
7type TodoKeys = 'title' | 'description'
8
9type TodoPreview = {
10  [P in TodoKeys]: Todo[P]
11}
12// TodoPreview 类型为:
13// {
14//   title: string
15//   completed: boolean
16// }
17

T[number]

T[number] 索引访问类型 用于 从数组类型 / 元组类型中提取所有元素的类型,最终得到一个联合类型。

  1. 普通数组类型
1// 定义普通数组类型
2type StringArr = string[];
3type NumberArr = number[];
4type BoolArr = boolean[];
5
6// T[number] 提取元素类型
7type Str = StringArr[number]; // 结果:string
8type Num = NumberArr[number]; // 结果:number
9type Bool = BoolArr[number]; // 结果:boolean
10
11// 等价于直接注解类型
12let s: Str = "hello"; // 等同于 let s: string
13let n: Num = 123;    // 等同于 let n: number
14let b: Bool = true;  // 等同于 let b: boolean
15
  1. 元组类型
1// 定义一个多类型的元组类型
2type Tuple = [123, "TS", true, null];
3
4// T[number] 提取所有元素的联合类型
5type TupleUnion = Tuple[number]; // 结果:123 | "TS" | true | null
6
7// 变量注解:可以是联合类型中的任意一种
8let val: TupleUnion;
9val = 123;    // 合法
10val = "TS";   // 合法
11val = true;   // 合法
12val = null;   // 合法
13val = false;  //  报错:不在联合类型中
14
  1. 字面量元组
1// 字面量元组:元素是数字/字符串字面量
2type StatusTuple = [200, 404, 500];
3type EnvTuple = ["dev", "test", "prod"];
4
5// 转字面量联合类型(开发中常用的枚举式类型)
6type Status = StatusTuple[number]; // 结果:200 | 404 | 500
7type Env = EnvTuple[number];       // 结果:"dev" | "test" | "prod"
8
9// 严格限制变量值,避免手写错误
10let code: Status = 200; // 合法
11code = 404;             // 合法
12code = 403;             //  报错:403 不在 200|404|500 
13
14let env: Env = "dev";   // 合法
15env = "prod";           // 合法
16env = "production";     //  报错:不在联合类型中
17
  1. as const + 数组 + T[number]

同时拥有数组的可遍历性 + 联合类型的严格类型约束。

1// 步骤1:用 as const 断言数组为「只读字面量元组」
2// 作用:让 TS 保留每个元素的字面量类型,且把数组转为只读元组(不可修改)
3const EnvArr = ["dev", "test", "prod"] as const;
4const StatusArr = [200, 404, 500] as const;
5
6// 步骤2:用 typeof 获取数组的类型(只读字面量元组类型)
7// 补充:typeof  TS 关键字,用于「从变量中提取其类型」
8type EnvTuple = typeof EnvArr; // 类型:readonly ["dev", "test", "prod"]
9type StatusTuple = typeof StatusArr; // 类型:readonly [200, 404, 500]
10
11// 步骤3:用 T[number] 转成字面量联合类型
12type Env = EnvTuple[number]; // 结果:"dev" | "test" | "prod"
13type Status = StatusTuple[number]; // 结果:200 | 404 | 500
14
15// 简化写法(开发中常用,省略中间元组类型)
16type EnvSimplify = typeof EnvArr[number];
17type StatusSimplify = typeof StatusArr[number];
18
  1. 泛型中使用 T[number]
1// 泛型 T 约束为「只读数组」(兼容 as const 断言的数组)
2function getUnionType<T extends readonly any[]>(arr: T): T[number] {
3  return arr[Math.floor(Math.random() * arr.length)];
4}
5
6// 传入 as const 断言的数组,返回值自动推导为字面量联合类型
7const res1 = getUnionType(["dev", "test", "prod"] as const); // res1 类型:"dev" | "test" | "prod"
8const res2 = getUnionType([1, 2, 3] as const); // res2 类型:1 | 2 | 3
9
10// 传入普通数组,返回值推导为基础类型
11const res3 = getUnionType([1, 2, 3]); // res3 类型:number
12
  1. 支持嵌套数组 / 元组
1const NestedArr = [[1, "a"], [2, "b"]] as const;
2type NestedUnion = typeof NestedArr[number]; // 结果:readonly [1, "a"] | readonly [2, "b"]
3type DeepUnion = typeof NestedArr[number][number]; // 结果:1 | "a" | 2 | "b"
4

测试用例

1/* _____________ 测试用例 _____________ */
2import type { Equal, Expect } from '@type-challenges/utils'
3
4const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const
5const tupleNumber = [1, 2, 3, 4] as const
6const sym1 = Symbol(1)
7const sym2 = Symbol(2)
8const tupleSymbol = [sym1, sym2] as const
9const tupleMix = [1, '2', 3, '4', sym1] as const
10
11type cases = [
12  Expect<Equal<TupleToObject<typeof tuple>, { 'tesla': 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y' }>>,
13  Expect<Equal<TupleToObject<typeof tupleNumber>, { 1: 1, 2: 2, 3: 3, 4: 4 }>>,
14  Expect<Equal<TupleToObject<typeof tupleSymbol>, { [sym1]: typeof sym1, [sym2]: typeof sym2 }>>,
15  Expect<Equal<TupleToObject<typeof tupleMix>, { 1: 1, '2': '2', 3: 3, '4': '4', [sym1]: typeof sym1 }>>,
16]
17
18// @ts-expect-error
19type error = TupleToObject<[[1, 2], {}]>
20
21

相关链接

分享你的解答:tsch.js.org/11/answer/z…查看解答:tsch.js.org/11/solution…更多题目:tsch.js.org/zh-CN

下面是我的公众号,欢迎关注。关注后有新的功能点会及时收到推送。

实战为王!专注于汇总各种功能点,致力于打造一系列能够帮助工程师实现各种功能的想法思路的优质文章。

前端功能点


type-challenges(ts类型体操): 11 - 元组转换为对象》 是转载文章,点击查看原文


相关推荐


Flutter艺术探索-Flutter国际化:多语言支持实现
kirk_wang2026/1/20

Flutter 国际化:从原理到实践的多语言支持方案 引言:为什么你的 Flutter 应用需要国际化? 如今,开发一款成功的应用就不得不考虑全球市场。国际化(i18n)和本地化(l10n)不再是可选项,而是连接不同文化用户的桥梁。对于使用 Flutter 的开发者来说,框架本身提供了强大的国际化支持,这不仅能显著提升用户体验,更是扩大应用市场份额的关键一步。想想看,当你的应用能够用用户的母语与其沟通时,下载量和用户留存率的提升是显而易见的。 Flutter 的国际化体系基于 Dart 的 in


mongodb的基本命令
豆浆粉牛奶2026/1/12

大家好我是小帅,今天学习mongodb的简单认识和基本命令。 本章内容: 理解MongoDB的业务场景、熟悉MongoDB的简介、特点和体系结构、数据类型等。能够在Windows和Linux下安装和启动MongoDB、图形化管理界面Compass的安装使用掌握MongoDB基本常用命令实现数据的CRUD 掌握MongoDB的索引类型、索引管理、执行计划。使用Spring DataMongoDB完成文章评论业务的开发 文章目录 1. MongoDB认识1.1 业务场景1.2 结构体系


AI 有你想不到,也它有做不到 | 2025 年深度使用 Cursor/Trae/CodeX 所得十条经验
Piper蛋窝2026/1/4

去年的今天,我还在奋笔疾书地写着 VS Code + Roo Cline 的评测心得:个人评测 | Cursor 免费平替:Roo Cline + DeepSeek-v3/Gemini-2.0 + RepoPrompt AI 辅助编程 。当时的我没有想过:在 2025 年, Roo Cline 会被我迅速淘汰,我也成为了 Cursor 这类 Vibe Coding 工具的稳定用户之一。 站在 2026 年伊始的节点上,审视自己的工作流,发现已经完全被锚定在了如下工具链上: 对话工具: Chat


别再让 AI 直接写页面了:一种更稳的中后台开发方式
月亮有石头2025/12/26

本文讨论的不是 Demo 级别的 AI 编码体验,而是面向真实团队、长期维护的中后台工程实践。 AI 能写代码,但不意味着它适合直接“产出页面”。 最近一年,大模型在前端领域的讨论几乎都围绕一个问题: “能不能让 AI 直接把页面写出来?” 在真实的中后台项目中,我的答案是: 不但不稳,而且很危险。 这篇文章想分享一种我在真实项目中实践过、可长期使用、可规模化的方式: 不是让 AI 写页面,而是把 AI 纳入中后台前端的工程体系中。 把 AI 的不确定性关进了笼子里,用工程流程保证可控性


小程序项目之驾校报名小程序源代码(java+vue+小程序+mysql)
风月歌2025/12/17

大家好我是风歌,曾担任某大厂java架构师,如今专注java毕设领域。今天要和大家聊的是一款java小程序项目——驾校报名小程序。项目源码以及远程配置部署相关请联系风歌,文末附上联系信息 。 项目简介: (1)管理员功能需求 管理员登陆后,主要包括首页、个人中心、用户管理、驾校教练管理、驾校信息管理、驾校报名管理、驾校车辆管理、预约教练管理、车辆预约管理、驾校考试管理、考试报名管理、课程安排管理、课程进度管理、系统管理等功能。 (2)用户功能需求 用户登陆后进入小程序首页,可以实现首页、通知


【云计算】云平台权限治理(六):企业项目的管理结构
大数据与AI实验室2025/12/9

《云平台权限治理》系列,共包含以下文章: 1️⃣ 云平台权限治理(一):虚拟数据中心 VDC2️⃣ 云平台权限治理(二):VDC 与企业项目3️⃣ 云平台权限治理(三):为什么公有云没有 VDC ?4️⃣ 云平台权限治理(四):VDC、企业项目、用户组5️⃣ 云平台权限治理(五):VDC 的树形管理结构6️⃣ 云平台权限治理(六):企业项目的管理结构 😊 如果您觉得这篇文章有用 ✔️ 的话,请给博主一个一键三连 🚀🚀🚀 吧 (点赞 🧡、关注 💛、收藏 💚)!!!您的支持


浅谈C++与C语言二进制文件差异(从一次链接错误说起)
码事漫谈2025/11/29

"undefined reference to `func' ",这个看似简单的链接错误背后,隐藏着C与C++二进制文件的根本差异。很多开发者认为C++只是"C with Classes",却不知这对"亲密兄弟"在二进制层面早已分道扬镳。 在软件开发的演进历程中,C++作为C语言的延伸,始终保持着高度的语法兼容性。这种表面上的相似性却掩盖了两者在编译产物层面的深刻差异。本文将从二进制文件的视角,深入剖析C++与C语言在目标代码生成机制上的本质区别,揭示面向对象、泛型编程等高级特性在机器层面的实现


MCP (Model Context Protocol) 技术理解 - 第二篇
想用offer打牌2026/2/8

引言 我们第一篇讲了MCP的基础概念、MCP解决的问题以及MCP的架构,我相信大家已经对MCP有了一定的了解,那么接下来让我们深入MCP具体是如何实现的,这一篇我们的重点放在通信协议和数据传输上,让我们一起来看看吧 如果你对前面的内容感兴趣,可以点击这里跳转 MCP (Model Context Protocol) 技术理解 - 第一篇 MCP的层级 MCP由两层组成: 数据层:定义了基于 JSON-RPC 的客户端-服务器通信协议,包括生命周期管理和核心原语,如工具、资源、提示和通知。 传输


WebMCP 时代:在浏览器中释放 AI 的工作能力
CharlesYu012026/2/16

随着 AI Agent 的广泛应用,传统的 Web 自动化与 Web 交互模式正在迎来根本性变化。WebMCP 是一个未来派的技术提案,它不仅改变了 AI 访问 Web 的方式,还为 AI 与前端应用之间建立起了 协议级的交互通道。本文从WebMCP架构分层解析这项技术及其工程意义。 面对 GEO 与 Agent 应用逐步弱化浏览器入口价值的趋势,浏览器厂商必须主动跟进,通过技术升级与生态重构来守住自身核心阵地。 一、WebMCP 是什么? WebMCP(Web Model Context P


告别死板流程:OpenSpec OPSX 如何重塑 SDD 开发工作流
fundroid2026/2/25

引言:SDD 与 OpenSpec 规范驱动开发(SDD)是什么? 近两年,AI 编码助手已经能“听懂人话”,从一段自然语言描述里生成大段代码。但很多团队也发现:如果需求只是散落在聊天记录里、脑补在每个人的心里,AI 很容易“发挥过度”——代码写出来了,却不是你真正想要的系统行为。 规范驱动开发(Spec-Driven Development,SDD)试图解决的,就是这个问题。它把规范(spec)而不是代码当成系统的“单一事实来源”:先用结构化、机器可读的方式,把系统应该做什么、有哪些边界和不变

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 XYZ博客