题目描述
在 JavaScript 中我们经常会使用可串联(Chainable/Pipeline)的函数构造一个对象,但在 TypeScript 中,你能合理的给它赋上类型吗?
在这个挑战中,你可以使用任意你喜欢的方式实现这个类型 - Interface, Type 或 Class 都行。你需要提供两个函数 option(key, value) 和 get()。在 option 中你需要使用提供的 key 和 value 扩展当前的对象类型,通过 get 获取最终结果。
例如
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
}
}
你只需要在类型层面实现这个功能 - 不需要实现任何 TS/JS 的实际逻辑。
你可以假设 key 只接受字符串而 value 接受任何类型,你只需要暴露它传递的类型而不需要进行任何处理。同样的 key 只会被使用一次。
题解
// ============= Test Cases =============
import type { Alike, Expect } from './test-utils'
declare const a: Chainable
const result1 = a
.option('foo', 123)
.option('bar', { value: 'Hello World' })
.option('name', 'type-challenges')
.get()
const result2 = a
.option('name', 'another name')
// @ts-expect-error
.option('name', 'last name')
.get()
const result3 = a
.option('name', 'another name')
// @ts-expect-error
.option('name', 123)
.get()
type cases = [
Expect<Alike<typeof result1, Expected1>>,
Expect<Alike<typeof result2, Expected2>>,
Expect<Alike<typeof result3, Expected3>>,
]
type Expected1 = {
foo: number
bar: {
value: string
}
name: string
}
type Expected2 = {
name: string
}
type Expected3 = {
name: number
}
// ============= Your Code Here =============
type Chainable = {
option(key: string, value: any): any
get(): any
}
代码实现
下面是实现Chainable类型的过程:
初始代码
type Chainable = {
option(key: string, value: any): any
get(): any
}
这是初始的Chainable类型定义,它接受一个 key 和 value,并返回any类型。get方法也返回 any 类型
引入泛型
为了存储对象的当前状态,我们引入泛型T,get 方法也返回泛型T
type Chainable<T> = {
option(key: string, value: any): any
get(): T
}
引入泛型T后,出现了报错,报错信息Generic type 'Chainable' requires 1 type argument(s).
我们需要给泛型T添加默认值
type Chainable<T = {}> = {
option(key: string, value: any): any
get(): T
}
定义 key 和 value 的类型
使用K和V泛型来表示键和值的类型,使用K extends PropertyKey对K进行约束,确保K是合法的键类型
type Chainable<T = {}> = {
option<K extends PropertyKey, V>(key: K, value: V): any
get(): T
}
实现链式调用
为了实现链式调用,option函数需要返回Chainable对象,并使用T & Record<K, V>将新的key、value合并到T中
type Chainable<T = {}> = {
option<
K extends PropertyKey,
V
>(
key: K,
value: V
): Chainable<T & Record<K, V>>
get(): T
}
此时可以看到result1已测试通过
const result1 = a
.option('foo', 123)
.option('bar', { value: 'Hello World' })
.option('name', 'type-challenges')
.get()
接着查看result2的错误提示,意思是不能设置重复的 key
const result2 = a
.option('name', 'another name')
// @ts-expect-error
.option('name', 'last name')
.get()
确保 key 不重复
为了确保 key 不重复,我们使用Exclude<K, keyof T>排除T中已存在的 key,避免在option函数中使用重复的 key,这样可以保证在类型层面上,不会将相同的 key 添加到对象中两次
type Chainable<T = {}> = {
option<
K extends PropertyKey,
V
>(
key: Exclude<K, keyof T>,
value: V
): Chainable<T & Record<K, V>>
get(): T
}
此时可以看到result2已测试通过,但是result3还是测试失败
const result3 = a
.option('name', 'another name')
// @ts-expect-error
.option('name', 123)
.get()
鼠标移到result3上查看result3的类型
const result3: {
name: string;
} & {
name: number;
}
预期的类型是{ name: number },所以只需要改成相同 key 时取最后一个 key 的类型
type Expected3 = {
name: number
}
处理相同 key 时取最后一个 key 的类型
先使用Omit移除T中已有的K键,再使用 Record<K, V>添加新的K键及其值,这样最终结果会取最后一个键的类型
type Chainable<T = {}> = {
option<
K extends PropertyKey,
V
>(
key: Exclude<K, keyof T>,
value: V
): Chainable<Omit<T, K> & Record<K, V>>
get(): T
}
这样,我们就完成了Chainable类型的实现