/*
  12 - 可串联构造器
  -------
  by Anthony Fu (@antfu) #中等 #application
  
  ### 题目
  
  在 JavaScript 中我们经常会使用可串联(Chainable/Pipeline)的函数构造一个对象,但在 TypeScript 中,你能合理的给它赋上类型吗?
  
  在这个挑战中,你可以使用任意你喜欢的方式实现这个类型 - Interface, Type 或 Class 都行。你需要提供两个函数 `option(key, value)` 和 `get()`。在 `option` 中你需要使用提供的 key 和 value 扩展当前的对象类型,通过 `get` 获取最终结果。
  
  例如
  
  ```ts
  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` 只会被使用一次。
  
  > 在 Github 上查看:https://tsch.js.org/12/zh-CN
*/

通过题目拆分。

  • 保存之前传入的对象 键值
  • 能够继续调用 options
  • 重复字段需要报错
  • get() 返回之前保存的类型

如何保存传入的类型值呢 ?

我们知道泛型可以设置默认值, 我们可以直接设置一个 {} 类型的默认值

type MyTyp<R = {}> = xxxx;

如何保证能够链式调用呢 ?

我们需要返回同样的类型 Chainable,同时要保存之前的操作。在TS中类型,可以调用自身,进行递归调用。

这里 extends 表示兼容的意思,(类型约束)用法和三元运算类似ValueType extends Condition ? Resolved : Rejected;,主要和 TS中 类型层级有关,可以理解成 子类 兼容 父类。

type MyType<R = {}> = R extends boolean ? MyType<R> : 0;

重复字段需要报错提示?

我们知道 never 不能被赋值给任何类型,如果赋值则会报错提示(因为 never 是最底层类型,可以理解成是所有类型的子类,不能够被其他类型兼容)。利用 这个特性,我们就能解决相同属性报错提示。

这里 keyof Obj 返回的是一个键的联合类型 "name" | "age" , 联合类型 只要满足其中一个就成立。

所以,"name" extends "name" | "age" 成立,key 类型被设置成 never,然后报错提示。

``

interface Obj {
  name: string;
  age: string;
}

function foo<T extends string, R>(key: T extends keyof Obj ? never : T, value: R) {}

foo("name", "") // error

解答

type Chainable<R = {}> = {
  option<K extends string, V>(key: K extends keyof R ? never : K, value: V): Chainable<Omit<R, K> & {[P in K]: V}>;
  get(): R;
}

Omit<T, K> 表示剔除 K 选中的属性,并返回剩余的属性。
[P in K]: V 会将 K 这个联合类型展开,并依次映射到 V 类型,[P in "name" | "age"]: string === {name: string, age: string}

Omit 内部实现

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

Exclued<T, K> 求 U 联合类型 相对于 T 联合类型的差集。
Pick<T, K> 表示提取 T 中选中属性集 U。

Pick 内部实现

type Pick<T, K extends keyof T> = { [P in K]: T[P] };

Exclude 内部实现

type Exclude<T, U> = T extends U ? never : T;

这里涉及了条件类型的 分布式属性,条件类型的 待比较值是 联合类型 时,会将联合类型展开依次比较然后,将结果组成联合类型。例如:Exclude<"name" | "age" | "info", "name" | "age"> === ("name" extends "name" | "age" ? never : "name") | ("age" extends "name" | "age" ? never : "age") | ("info" extends "name" | "age" ? never : "info") === "info",不过需要注意的是,分布式特性需要 联合类型做泛型 传入条件类型中,才会有分布式属性,直接 "name" | "age" extends "name" 是不会发生的。