10 - 元组转合集
by Anthony Fu (@antfu) #中等 #infer #tuple #union
题目
实现泛型TupleToUnion<T>,它返回元组所有值的合集。
例如
1type Arr = ['1', '2', '3'] 2 3type Test = TupleToUnion<Arr> // expected to be '1' | '2' | '3' 4
在 Github 上查看:tsch.js.org/10/zh-CN
代码
1/* _____________ 你的代码 _____________ */ 2 3type TupleToUnion<T> = T extends [infer F, ...infer R] ? F | TupleToUnion<R> : never 4 5
关键解释:
T extends [infer F, ...infer R]用于判断元组是否为空。F | TupleToUnion<R>用于递归处理元组的剩余部分。never用于处理空元组的情况。
相关知识点
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}`); 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
infer
infer 是 TypeScript 在条件类型中提供的关键字,用于声明一个 待推导的类型变量(类似给类型起一个临时名字),只能在 extends 子句中使用。它的核心作用是:从已有类型中提取 / 推导我们需要的部分,而无需手动硬编码类型。
infer 必须配合条件类型使用,语法结构如下:
1// 基础结构:推导 T 的类型为 U,若能推导则返回 U,否则返回 never 2type InferType<T> = T extends infer U ? U : never; 3 4type Example = InferType<string>; // Example 类型为 string 5type Example2 = InferType<number[]>; // Example2 类型为 number[] 6
高频使用场景:
1. 提取函数的返回值类型
1// 定义类型工具:提取函数的返回值类型 2type GetReturnType<Fn> = Fn extends (...args: any[]) => infer R ? R : never; 3 4// 测试用函数 5const add = (a: number, b: number): number => a + b; 6const getUser = () => ({ name: "张三", age: 20 }); 7 8// 使用类型工具 9type AddReturn = GetReturnType<typeof add>; // AddReturn 类型为 number 10type UserReturn = GetReturnType<typeof getUser>; // UserReturn 类型为 { name: string; age: number } 11
2. 提取数组的元素类型
1// 定义类型工具:提取数组元素类型 2type GetArrayItem<T> = T extends (infer Item)[] ? Item : never; 3 4// 测试 5type NumberArray = GetArrayItem<number[]>; // NumberArray 类型为 number 6type StringArray = GetArrayItem<string[]>; // StringArray 类型为 string 7type MixedArray = GetArrayItem<[string, number]>; // MixedArray 类型为 string | number 8
3. 提取 Promise 的泛型参数类型
1// 定义类型工具:提取 Promise 的泛型类型 2type GetPromiseValue<T> = T extends Promise<infer Value> ? Value : never; 3 4// 测试 5type PromiseString = GetPromiseValue<Promise<string>>; // PromiseString 类型为 string 6type PromiseUser = GetPromiseValue<Promise<{ id: number }>>; // PromiseUser 类型为 { id: number } 7
4. 提取函数的参数类型
1// 定义类型工具:提取函数参数类型 2type GetFunctionParams<Fn> = Fn extends (...args: infer Params) => any ? Params : never; 3 4// 测试 5const fn = (name: string, age: number): void => {}; 6type FnParams = GetFunctionParams<typeof fn>; // FnParams 类型为 [string, number] 7 8// 进一步:提取第一个参数的类型 9type FirstParam = GetFunctionParams<typeof fn>[0]; // FirstParam 类型为 string 10
|
| 运算符用于表示联合类型,即一个值可以是多个类型中的任意一个。
- 变量的联合类型注解
1// 变量 a 可以是字符串 OR 数字 2let a: string | number; 3 4// 合法赋值(符合任意一种类型) 5a = "TS"; 6a = 123; 7 8// 非法赋值(不属于联合类型中的任何一种),TS 直接报错 9a = true; // ❌ 类型 'boolean' 不能赋值给类型 'string | number' 10
- 函数参数的联合类型
1// 函数接收 string 或 number 类型的参数 2function printValue(val: string | number) { 3 console.log(val); 4} 5 6// 合法调用 7printValue("hello"); 8printValue(666); 9 10// 非法调用,TS 报错 11printValue(null); // ❌ 12
- 数组的联合类型(注意两种写法的区别)
1// 写法1:(A | B)[] —— 数组的「每个元素」可以是 A 或 B(混合数组) 2let arr1: (string | number)[] = [1, "2", 3, "4"]; // 合法 3 4// 写法2:A[] | B[] —— 「整个数组」要么全是 A 类型,要么全是 B 类型(纯数组) 5let arr2: string[] | number[] = [1, 2, 3]; // 合法(全数字) 6arr2 = ["1", "2", "3"]; // 合法(全字符串) 7arr2 = [1, "2"]; // ❌ 报错:混合类型不符合要求 8
当使用联合类型的时候,访问某一个子类型的专属属性 / 方法时,需要进行类型守卫,可用的方法有 typeof 、in 、switch 、instanceof 。
typeof
1function getLength(val: string | number) { 2 // 类型窄化:判断 val 是 string 类型 3 if (typeof val === "string") { 4 // 此分支中,TS 确定 val 是 string,可安全使用 length 5 return val.length; 6 } else { 7 // 此分支中,TS 确定 val 是 number,执行数字相关逻辑 8 return val.toString().length; 9 } 10} 11 12console.log(getLength("TS")); // 2 13console.log(getLength(1234)); // 4 14
in
1function printUserInfo(user: { name: string } | { age: number }) { 2 // 类型窄化:判断 user 是否有 name 属性(即是否是 { name: string } 类型) 3 if ("name" in user) { 4 console.log(`Name: ${user.name}`); 5 } else { 6 // 此分支中,TS 确定 user 是 { age: number } 类型 7 console.log(`Age: ${user.age}`); 8 } 9} 10
switch
1interface User { 2 type: "user"; 3 name: string; 4 age: number; 5} 6interface Admin { 7 type: "admin"; 8 name: string; 9 permission: string[]; 10} 11// 联合类型:可以是 User 或 Admin 12type Person = User | Admin; 13function printPerson(p: Person) { 14 switch (p.type) { 15 case "user": 16 console.log(p.age); // 确定是 User 17 break; 18 case "admin": 19 console.log(p.permission); // 确定是 Admin 20 break; 21 } 22} 23
instanceof
1// 定义两个类 2class Dog { 3 bark() { console.log("汪汪"); } 4} 5class Cat { 6 meow() { console.log("喵喵"); } 7} 8 9// 联合类型:Dog 或 Cat 实例 10type Animal = Dog | Cat; 11 12// instanceof 类型守卫(针对类实例) 13function animalCall(animal: Animal) { 14 if (animal instanceof Dog) { 15 animal.bark(); 16 } else { 17 animal.meow(); 18 } 19} 20 21animalCall(new Dog()); // 汪汪 22animalCall(new Cat()); // 喵喵 23
测试用例
1/* _____________ 测试用例 _____________ */ 2import type { Equal, Expect } from '@type-challenges/utils' 3 4type cases = [ 5 Expect<Equal<TupleToUnion<[123, '456', true]>, 123 | '456' | true>>, 6 Expect<Equal<TupleToUnion<[123]>, 123>>, 7] 8 9
相关链接
分享你的解答:tsch.js.org/10/answer/z…查看解答:tsch.js.org/10/solution…更多题目:tsch.js.org/zh-CN
下面是我的公众号,欢迎关注。关注后有新的功能点会及时收到推送。
实战为王!专注于汇总各种功能点,致力于打造一系列能够帮助工程师实现各种功能的想法思路的优质文章。
