type-challenges:Chainable Optionshttps://github.com/type-cha
Chainable Options
问题描述
在 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 =============
// 答案1
type Chainable<T = {}> = {
option<K extends string, V>(
key: Exclude<K, keyof T>,
value: V
): Chainable<Omit<T, K> & Record<K, V>>
get(): T
}
// 答案2 自己实现 Exclude 方法
type Chainable<T = {}> = {
option: <K extends string, V>(
key: K extends keyof T ? never : K,
value: V
) => Chainable<Omit<T, K> & Record<K, V>>
get: () => T
}
这里首先可以确定两点,第一,类型 Chainable
中包含两个函数,第一个函数是 option
,该函数有两个形参,第一个为键名,第二个为键值。第二个函数是 get
,无形参,用来获取最终的 Chainable
的类型。
先来看 option
,从题目 const result1 = a.option('foo', 123)
可以明确的是,a
返回的应该是个对象,所以才可以使用点运算符 ,其次,所有的键值对应该记录在一个对象内,所以 Chainable
一开始应该有个空对象,最后,get
应该获取到整个对象,所以 get
最简单,应该返回这个空对象,如果使用 javascript
来完成的话,应该是这样:
function Chainable() {
let obj = {}
function option(key, value) {
obj[key] = value;
return this;
}
function get() {
return obj;
}
return {
option,
get
}
}
这里首先先不看键名可以被修改和覆盖的问题,只看当前的基础功能实现,改为 typescript
时,应该如何定义这里的 obj
呢,相信你已经想到了,就是使用泛型参数,下面是改为 typescript
的基础写法:
type Chainable<T = {}>= {
option(key: string,value: any): Chainable<T>
get(): T
}
这里你可能会疑惑,不对啊,那 javascript
中 obj[key] = value
这一步,怎么没在这里面体现出来呢,确实,这里 option
返回的还是 Chainable<T>
,这里的 T
还是空对象,那应该怎么将当前的键值对放入其中的,这就需要我们给 option
接着定义泛型参数了:
type Chainable<T = {}> = {
option<K extends string, V>(key: string, value: V): Chainable<T & Record<K, V>>
get(): T
}
这一步可能稍微有一点难理解,泛型 K
是键名,V
是键值,Record<K, V>
代表当前键值对的对象,比如:
type obj = Record<'name', 'rixleft'>
// 鼠标放在 obj 上,可以看到这里的 type obj = {name: "rixleft"}
T & Record<K, V>
相当于 javascript
中的 obj[key] = value
这一步。
最后我们只需要实现当添加了一个键值对之后,如果键名已经存在在泛型对象T
中,在返回的 Chainable
中应排除当前键值对,添加修改后的键值对,这样在测试用例3的时候才会通过,并且新增的时候也不可以使用当前的键名,否则会报错。所以需要使用 Exclude
排除已经添加过的键名。
type Chainable<T = {}> = {
option<K extends string, V>(
key: Exclude<K, keyof T>,
value: V
): Chainable<Omit<T, K> & Record<K, V>>
get(): T
}
转载自:https://juejin.cn/post/7385487628457803839