# 12-可串联构造器
# 题目描述
在 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
只会被使用一次。
# 分析
这个题目虽然看着长,但是也算是比较贴合平时的工作了。一般而言,静下心来仔细分析就可以得到结果了。
从题目看,目标的类型至少具备两个属性,option
和 get
。
其中 option
和 get
都是函数,但是 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
类型。
这里需要注意的就是 K
和 S
的写法和位置。
但是这样写并不能通过本题目的用例:
const result3 = a
.option('name', 'another name')
// @ts-expect-error
.option('name', 123)
.get()
Expect<Alike<typeof result3, Expected3>>,
type Expected3 = {
name: number
}
首先,对于传入相同属性值的地方,需要提醒用户输入非法,同时虽然输入非法,但是类型要以最后输入的为准(这就是用例的要求,实际并无太大意义)。要想达到和题目一样的效果,就需要:
- 限制入参的类型,不能为
keyof T
- 修改
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>;
在 Case2 中,never。这里肯定有同学要问,为什么需要 Merge 下才会出现 never,别问,问就是就是这样,这也算是一个规则吧,A & B, ts 在代码提醒的时候并不会展示合并的结果,只有 Merge 一份的时候,才能正确的展示交叉后的结果。更深层次的探讨就不在范畴了,记住即可。
所以要想实现题目的效果,就需要避免交叉。而是在交叉前在原属性里面排除掉要增加的属性即可。直接看答案。
# 题解
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 一节。
# 知识点
- 递归解决嵌套问题
- 实现 Omit