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// 约束 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// 基础示例:判断类型是否为字符串 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
- 配合
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// 基础接口 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
- 类继承:复用父类的属性 / 方法
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// 定义带只读属性的接口 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
- 作用于类的属性: 类中使用
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
- 作用于数组 / 元组(只读数组):
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
- 结合
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
- 只读索引签名:如果类型使用索引签名,也可以标记为
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// 定义普通数组类型 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// 定义一个多类型的元组类型 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// 字面量元组:元素是数字/字符串字面量 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
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
- 泛型中使用
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
- 支持嵌套数组 / 元组
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
下面是我的公众号,欢迎关注。关注后有新的功能点会及时收到推送。
实战为王!专注于汇总各种功能点,致力于打造一系列能够帮助工程师实现各种功能的想法思路的优质文章。