likes
comments
collection
share

type-challenges:Chainable Optionshttps://github.com/type-cha

作者站长头像
站长
· 阅读数 21

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
}

这里你可能会疑惑,不对啊,那 javascriptobj[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
评论
请登录