题目描述 🎯
在JavaScript中,我们经常会使用可串联的构造函数对象,现在需要在TypeScript中实现同样的功能。
要求实现一个可串联调用的类型Chainable,使得每个调用都能正确推导出类型。
declare const config: Chainable;
const result = config
.option('foo', 123)
.option('name', 'type-challenges')
.option('bar', { value: 'Hello World' })
.get()
// 期望 result 的类型是:
interface Result {
foo: number
name: string
bar: {
value: string
}
}
题目分析 📝
-
需要实现的功能:
option方法:添加配置项get方法:获取最终结果
-
类型要求:
- 正确推导每个属性的类型
- 支持链式调用
- 支持对象类型的值
解题思路 💡
1. 使用泛型记录已添加的配置项
2. 每次调用`option`时合并新的配置项
3. 使用`get`方法返回最终类型
代码实现 ⌨️
type Chainable<T = {}> = {
option<K extends string, V>(
key: K extends keyof T ? never : K,
value: V
): Chainable<T & { [P in K]: V }>
get(): T
}
解题详情 🔍
- 类型结构分析
type Chainable<T = {}> = {
option<K extends string, V>(...): ...
get(): T
}
- `T`用于存储累积的配置项类型
- 初始值为空对象`{}`
2. option方法类型分析
option<K extends string, V>(
key: K extends keyof T ? never : K,
value: V
): Chainable<T & { [P in K]: V }>
- `K extends string`:键必须是字符串
- `K extends keyof T ? never : K`:防止重复键
- `T & { [P in K]: V }`:合并新的配置项
3. get方法类型分析
get(): T
- 返回累计的所有配置项类型
示例分析 🌟
declare const config: Chainable
const result = config
.option('foo', 123)
.option('name', 'type-challenges')
.option('bar', { value: 'Hello World' })
.get()
// 执行步骤分析:
// 1. 初始状态
type T0 = {}
// 2. 添加 foo
type T1 = { foo: number }
// {
// foo: number
// }
// 3. 添加 name
type T2 = T1 & { name: string}
// {
// foo: number
// name: string
// }
// 4. 添加 bar
type T3 = T2 & { bar: { value: string } }
// {
// foo: number
// name: string
// bar: {
// value: string
// }
// }
// 5. 调用 get()
// 返回 T3 类型
进阶实现 🚀
如果我们想支持覆盖已存在的键,可以这样修改:
type Chainable<T = {}> = {
option<K extends string, V>(
key: K,
value: V
): Chainable<{
[P in keyof T as P extends K ? never : P]: T[P]
} & { [P in K]: V }>
get(): T
}
解题详解 🔍
- 映射类型部分
[P in keyof T as P extends K ? never : P]: T[P]
- 遍历原有类型
T的所有键 - 使用
as子句过滤键 - 如果键与新键相同,返回
never - 否则保留原有键
- 新属性部分
{ [P in K]: V }
- 添加新的键值对
3.使用交叉类型合并
type1 & type2
示例分析 🌟
declare const config: Chainable
const result = config
.option('foo', 123)
.option('foo', 'hello') // 覆盖之前的 foo
.option('bar', true)
.get()
// 执行步骤分析:
// 1. 第一次调用 .option('foo', 123)
type Step1 = {
foo: number
}
// 2. 第二次调用 .option('foo', 'hello')
// 2.1 过滤现有属性
type Filtered = {
// foo extends 'foo' ? never : 'foo' => never
// 所以没有保留任何旧属性
}
// 2.2 添加新属性
type Step2 = Filtered & {
foo: string
}
// 3. 调用 .option('bar', true)
// 3.1 过滤现有属性
type Filtered2 = {
// 保留 foo,因为 foo extends 'bar' 为 false
foo: string
}
// 3.2 添加新属性
type Step3 = Filtered2 & {
bar: boolean
}
// 最终类型
type Result = {
foo: string
bar: boolean
}