可串联构造器

113 阅读2分钟

题目描述 🎯

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
  }
}

题目分析 📝

  1. 需要实现的功能:

    • option方法:添加配置项
    • get方法:获取最终结果
  2. 类型要求:

    • 正确推导每个属性的类型
    • 支持链式调用
    • 支持对象类型的值

解题思路 💡

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
}

解题详情 🔍

  1. 类型结构分析
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
}

解题详解 🔍

  1. 映射类型部分
[P in keyof T as P extends K ? never : P]: T[P]
  • 遍历原有类型T的所有键
  • 使用as子句过滤键
  • 如果键与新键相同,返回never
  • 否则保留原有键
  1. 新属性部分
{ [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
}