gpt4 book ai didi

typescript - 构建推断对象会起作用,直到它不起作用为止

转载 作者:行者123 更新时间:2023-12-02 02:23:44 27 4
gpt4 key购买 nike

Playground | Sandbox

让我们从这个快照开始结束,它描述了所有通过的单元测试,但以红色突出显示,不幸的是类型保真度的损失:

enter image description here

您看到的是 Configurator()函数提供了一种构建配置的方法,完成后您可以调用 done()并且键入的配置可用。好消息是它几乎可以工作了。它确实在运行时工作(注意蓝色文本,它显示所有键 - a、b 和 c - 确实已设置)。但是,正如您在底部圆圈区域中看到的,属性 b 输入,而 ac是。

错过输入 b 的原因来自实现细节,这些细节在某种程度上是可以理解的,但让我们来讨论它们。这是Configurator代码:

export function Configurator<I extends object>(initial?: I) {
let configuration = () => initial || {};

const api = <C extends object>(current: C): IConfigurator<C> => {
return {
set<V, K extends string, KV = { [U in K]: V }>(key: K, value: V) {
const keyValue = ({ [key]: value as V } as unknown) as KV;

const updated = { ...config, ...keyValue };
configuration = (): C & KV => updated;

return api<C & KV>(updated);
},
done() {
return configuration() as C;
},
};
};

return api(initial || {});
}

界面IConfigurator<T>每次配置循环都会返回 api表面和使用set()能够添加新的键值对,因为它们具有完整的 Typescript 保真度。这部分有效,这就是变量 c 的原因记住属性“a”(参见上面的第 1 项),以及为什么变量 d记住属性“a”(因为 c 知道它)和属性“c”。

属性“b”(在突出显示为 #2 的行上设置)会在运行时被记住,因为 configuration在配置外壳中分配的参数与api的外壳。我们最初只是在对象中分配了值,但是您在这里看到的是一个返回值的函数;我们改变了这一点,因为有时 Typescript 在涉及函数时更擅长推断事物。可惜不在这里。配置类型被固定为一个空对象。因为api表面类型C我们可以将配置转换为类型 C但这意味着类似 b 的属性从类型系统的推断中被孤立。

我的问题有两个:

  1. 有没有一种方法可以让我们捕获/记住 configuration 的类型这样当我们调用done()时我们如何确保提供完整的类型支持?
  2. 或者,如果类型configuration已经丢失了有办法重建吗?正如您从屏幕截图中看到的,在运行时可以识别属性及其类型...似乎这应该导致一种实现完整类型支持的方法,但迄今为止的尝试都失败了(见下文)

注意:这是我们为实现上面的#2 所做的尝试。

export type AppendToObject<T, U extends keyof any, V> = {
[K in keyof T | U]: K extends keyof T ? T[K] : V;
};

/** Given a structured run-time object, iterate over keys and append types */
export const inferObject = <T extends object>(v: T) => {
let obj = v;

Object.keys(v).forEach((key) => {
const value = (v as any)[key];
obj = fixup(v, key, value);
});

return obj;
};

function fixup<V, T extends object, K extends string = string>(obj: T, key: K, value: V) {
return { ...obj, [key]: value } as AppendToObject<T, K, V>;
}


注2:IConfigurator的代码被遗漏了,虽然如果我们找到解决方案,它需要更新,但我不认为这是问题的根源。尽管如此,我想不出任何人应该相信我这一点的理由。

interface IConfigurator<C> {
set<V, K extends string, KV = { [U in K]: V }>(key: K, value: V): IConfigurator<C & KV>;
done(): C;
}

最佳答案

TypeScript 不支持既返回值又缩小调用该方法的对象类型范围的方法。

传统上(TS 3.7 之前)我想说,如果您希望 TypeScript 的类型检查器正确跟踪类型,您需要使用纯粹的“流畅构建器”模式,其中链中的每个值仅使用一次。或者,如果您确实多次使用它们(例如您的 c.set() 示例),则需要使这些方法不可变,以便 c.set()返回值是唯一受影响的。传统上,Typescript 无法捕获对象方法改变其值状态的想法。

从 TypeScript 3.7 开始,您可以使用 assertion functions编写一个配置器,进行相反的操作;您忽略该方法的返回值,并继续重新使用原始对象。每次调用c.set()时,它都会缩小c的类型。

您当前的 set() 方法旨在执行这两件事,但 TypeScript 只能真正支持其中之一。 (有状态版本有一个恼人的警告)


纯构建器版本与现有的 set() 方法的工作方式如下:

let o = Configurator()
.set("a", 5)
.set("b", "foobar")
.set("c", { hello: "world" })
.done();

o.a // okay
o.b // okay
o.c // okay

在这里,我们在使用一次中间对象后就将其丢弃;我们从不重复使用它。


有状态方法使用 TypeScript 3.7 中引入的断言函数。这些函数允许您将 void 返回函数标记为对其参数之一的类型具有缩小效果……或者在方法的情况下,对对象类型具有缩小效果有方法。我将更改您的 set() 方法的签名:

interface IConfigurator<C = {}> {
add<A extends {}>(dictionary: A): IConfigurator<A & C>;
set<V, K extends string, KV = { [U in K]: V }>(
key: K, value: V
): asserts this is IConfigurator<C & KV>;
done(): C;
}

注意断言这是...返回类型。然后你可以像这样使用配置器:

let c: IConfigurator = Configurator(); 
// ~~~~~~~~~~~~~~~ <-- note this annotation
c.set("a", 5);
c.set("b", "foobar");
c.set("c", { hello: "world" });

const o = c.done();
o.a // okay
o.b // okay
o.c // okay

每次调用 c.set() 时,都会缩小 c 的类型,这样当您最终调用 c.done(),已知返回值具有您设置的所有属性。令人沮丧的是,您需要为 c 提供显式类型注释才能使其工作。如果您将其关闭,则每次调用 set() 时都会出现令人讨厌的错误(有关详细信息,请参阅 microsoft/TypeScript#36931):

let cBad = Configurator();
cBad.set("a", 5); // error!
//~~~~~~ <-- Assertions require every name in the call target
// to be declared with an explicit type annotation.

Playground link to code

关于typescript - 构建推断对象会起作用,直到它不起作用为止,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/66035496/

27 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com