# 可串联构造器

# 题目描述

在 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 只会被使用一次。

# 分析

从题目看,目标的类型至少具备两个属性,optionget

其中 optionget 都是函数,但是 option 的返回,还是一个类似的结构,只不过新的类型中 get 的返回值多了一些属性。可以写出如下的类型:

type Chainable<T = {}> = {
  option: <K extends string, S>(
    key: K,
    value: S,
  ) => Chainable<
    {
      [P in keyof T]: T[P];
    } & {
      [P in K]: S;
    }
  >;
  get(): T;
};

option 的返回值通过递归构造一个新增了属性后的新的 Chainable 类型。

这里需要注意的就是 KS 的写法和位置。

但是这样写并不能通过本题目的用例:

const result3 = a
  .option('name', 'another name')
  // @ts-expect-error
  .option('name', 123)
  .get()

Expect<Alike<typeof result3, Expected3>>,

type Expected3 = {
  name: number
}

首先,对于传入相同属性值的地方,需要提醒用户输入非法,同时虽然输入非法,但是类型要以最后输入的为准(这就是用例的要求,实际并无太大意义)。要想达到和题目一样的效果,就需要:

  1. 限制入参的类型,不能为 keyof T
  2. 修改 option 出参的类型,以最后一次为准

第一点比较容易做到,只需要 (k: K extends keyof T ? never : K) 即可限制,第二点需要绕个弯,因为 ts 中 & 类型,并不能后者覆盖前者,假设 A & B, 那么当 A 和 B 具有同一属性名时,其属性值也会合并,如下:

type Case1 = { a: number } & { a: string };

// Case2: never
type Case2 = Case1['a'];

type Merge<T> = {
  [P in keyof T]: T[P];
};

// Case3 = { a: never }
type Case3 = Merge<Case1>;

# 题解

type Chainable<T = {}> = {
  option<K extends string, S>(
    key: K extends keyof T ? never : K,
    value: S,
  ): Chainable<
    {
      // 核心,从原来的 T 中排除 K 属性,这样交叉后的结果就是传入的 S 属性
      [P in keyof T as P extends K ? never : P]: T[P];
    } & {
      [P in K]: S;
    }
  >;
  get(): T;
};

核心就是上述的注释,将 { [P in keyof T as P extends K ? never : P] : T[P]} 替换成 Omit 也是一样的效果,本质就是剔除当前类型中,属性为 K 的元素,可以查看 实现 Omit 一节。

# 知识点

  1. 递归解决嵌套问题
  2. 实现 Omit