Typescript 构建推断对象直到它不';T

Typescript 构建推断对象直到它不';T,typescript,Typescript,| 让我们从这个快照开始,它描述了所有通过的单元测试,但以红色突出显示,这是一个不幸的类型保真度损失: 您看到的是一个Configurator()函数,它提供了一种建立配置的方法,完成后,您可以调用done(),类型化配置可用。好消息是它几乎可以工作。它确实在运行时工作(注意蓝色文本,它显示所有键——a、b和c——都已设置)。但是,正如您在底部的圆圈区域中所看到的,属性b未键入,而a和c则未键入 在为b键入时出现这种遗漏的原因来自于实现细节,这些细节在某种程度上是可以理解的,但让我们来讨论一下

|

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

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

在为
b
键入时出现这种遗漏的原因来自于实现细节,这些细节在某种程度上是可以理解的,但让我们来讨论一下。以下是
配置程序
代码:

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 || {});
}

注2:IConfigurator的代码没有启动,如果我们找到解决方案,它需要更新,但我认为这不是问题的根源。尽管如此,我想不出任何理由让任何人相信我

interface IConfigurator<C> {
  set<V, K extends string, KV = { [U in K]: V }>(key: K, value: V): IConfigurator<C & KV>;
  done(): C;
}
接口IConfigurator{
设置(键:K,值:V):IConfigurator;
完成():C;
}

我相信您遇到了这个问题:

遗憾的是,我认为没有一个简单的解决办法。

我相信这就是为什么大多数构建器都是从已知的完整类型开始的。

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

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

由于TypeScript 3.7,您可以使用它编写一个配置程序,您可以在其中执行相反的操作;忽略方法的返回值,并继续重新使用原始对象。每次调用
c.set()
,它都会缩小
c
的类型

您当前的
set()
方法旨在完成这两项工作,但TypeScript只能真正支持其中一项。(有状态版本有一个恼人的警告)


pure builder版本与现有的
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中介绍的断言函数。这些函数允许您将返回的函数标记为对其中一个参数的类型具有缩小效应。。。或者在方法的情况下,对具有该方法的对象类型的缩小效果。我要更改您的
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;
}
每次调用
c.set()。令人沮丧的是,您需要为
c
提供一个显式的类型注释,这样才能工作。如果不使用该选项,则每次调用
set()
(有关更多信息,请参阅):

让cBad=Configurator();
cBad.set(“a”,5);//错误!

//~~~~~~看起来你在制作IDE的映像时遇到了麻烦,但是@jcalz为什么?我见过其他人这样回答,但这并不是代替代码,而是在代码之外,并帮助使问题更加清楚。否则将无法显示测试和运行时控制台的结果,也不可能突出显示某些区域。请考虑修改上面的代码和图像,以构成适合于独立的IDE,以便其他人能够很容易地演示其自身的问题。当我可以从问题开始回答问题时,回答问题要容易得多,而不必一开始就花费精力去重现问题。当然,我们无意复制和粘贴这些问题。图片的使用方式不同。你所描述的是为什么图片不应该代替代码,但事实上所有相关的代码都被添加为文本。它们确实如此!可悲的是,我唯一能向你证明这一点的方法就是截图。有一些断言函数可以实现这一点,但是它们不能毫无痛苦地使用。我总是从一个完全类型的配置开始,但在某些情况下,我的库会将配置程序推出我的库的消费者手中,然后对更大的动态行为的渴望变得非常有价值。通读这篇文章,这对看到我生命的最后一周带来了很大的帮助被他人的想法和欲望聚焦。谢谢你的贡献。虽然我没有问最初的问题,但这是一个非常方便的阅读,在我以前没有接触过的打字稿的一面。非常感谢你花时间写这篇文章。
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
let cBad = Configurator();
cBad.set("a", 5); // error!
//~~~~~~ <-- Assertions require every name in the call target
// to be declared with an explicit type annotation.