【问题标题】:How can I have a do a type that is Partial<Foo> but passed references ONLY include keys from Foo我怎样才能做一个 Partial<Foo> 但传递的引用只包括来自 Foo 的键的类型
【发布时间】:2023-03-30 06:15:02
【问题描述】:

假设我有

class Foo {
   readonly a!: string
   readonly b!: string
   readonly c!: string
}

然后我创建一个带有参数的方法

  Partial<Foo>

问题是我也有

class Bar {
   readonly a!: string
   readonly d!: string
}

我可以将Bar 传递给采用Partial&lt;Foo&gt; 的方法,但这不是我想要的。

我想要的是某种 AllowOnlyKeysFrom&lt;Partial&lt;Foo&gt;&gt;,你仍然可以有 0 个键、1 个键或所有键,但 Bar 不会匹配,因为它有一个不在 foo 中的键。

我尝试过使用多个库,包括 ts-essentialsts-typedefsts-toolbelt,但还没有找到解决方案。

他们有什么方法可以得到我想要的行为,所以编译器会阻止我将完全错误的对象传递给mymethod( part: Partial&lt;Foo&gt; )

【问题讨论】:

  • stackoverflow.com/questions/49580725/… 可能是相关的。我从最佳答案中尝试了NoExtraProperties&lt;Partial&lt;Foo&gt;&gt;,但Partial 类型阻止它按预期工作。该页面上涉及鉴别器属性的其他解决方案可能是您最好的选择。

标签: typescript


【解决方案1】:

你想要exact types,它在 TypeScript 中并不存在。 TypeScript 中的对象类型被认为是“开放的”或“可扩展的”; {a?: string, b?: string, c?: string} 之类的类型仅​​指定 abc 键处的属性类型。它根本不禁止其他键。所以像{a: "", b: "", c: "", d: 12345} 这样的值是有效的。因此Bar 是一个有效的Partial&lt;Foo&gt;

请注意,这通常是一个理想的功能,因为它允许 interfaceclass 扩展:

interface X {
    x: string;
}
interface Y extends X {
    y: number;
}

这里,Y extends X 表示每个Y 一个X。这直接导致您不满意的那种过度扩张的财产:

const y: Y = { x: "", y: 1 }
const x: X = y; // okay

有一些近似精确类型的变通方法,它们的表现可能足以满足您的目的,但都可以规避,因为编译器总是会让您将值扩大到不知道有问题的键的类型。


我首选的解决方法是将myMethod() generic 设置为Partial&lt;Foo&gt; 类似T 的类型part。您可以constrainT,这样其中的每个已知属性都必须是来自Foo 的属性,如果有任何额外的属性,它们必须是never。像这样:

mymethod<T extends { [K in keyof T]: K extends keyof Foo ? Foo[K] : never }>(
    part: T
) { 
    console.log(part);
}

并测试一下:

obj.mymethod({a: ""}) // okay

const bar: Bar = { a: "", d: "" };
obj.mymethod(bar); // error!
// Types of property 'd' are incompatible.

万岁,它可以防止Bar!如果您将对象交给mymethod,那就太好了,已知 有多余的属性。但没有什么能阻止这一点:

const partialFoo: Partial<Foo> = bar; // valid assignment
obj.mymethod(partialFoo); // no error!

这里,partialFoo 被显式注释为Partial&lt;Foo&gt;bar 被分配给它。之所以成功,是因为 TypeScript 中的类型是开放的,而不是精确的。而且编译器已经完全忘记了partialFoo来自Bar;它只将其视为Partial&lt;Foo&gt;。所以编译器的 known 键没问题,而有问题的键是编译器 unknown 的,所以它不会捕获它。


在这一点上,我想说你可能想退后一步,考虑使用 TypeScript 的类型系统而不是反对它。如果 TypeScript 将对象类型视为开放的,那么您的代码也应该这样做,只对已知键进行显式操作。想象一下,我们有一个函数pluck(),它产生一个只有我们指定的键的对象:

function pluck<T, K extends keyof T>(t: T, ...k: K[]): Pick<T, K> {
    return k.reduce((a, k) => (k in t && (a[k] = t[k]), a), {} as Pick<T, K>);
}

然后创建一个 mymethod() 的版本,在对其进行操作之前,plucks 来自 partabc 属性:

mySafeMethod(part: Partial<Foo>) {
    this.mymethod(pluck(part, "a", "b", "c"));
}

在这种情况下,如果你传入一个Bar,就不再是问题了:

obj.mySafeMethod(bar); // { a: "" };

运行时代码预计part 可能具有比abc 更多的属性,并显式忽略它们,而不是例如使用Object.keys() 或@ 迭代所有现有键987654374@。这可能是不那么惯用的 JavaScript,但它确实使编译器更容易处理。


好的,希望其中一个想法对您有所帮助;祝你好运!

Playground link to code


更新

TypeScript 并不能真正“获取”确切的类型,尤其是未指定的泛型类型,例如您在 mymethod 的实现中看到的类型。所以在mymethod 内部,编译器对part 了解不多。有办法解决这个问题......一种更丑陋的方法是将确切类型与开放类型相交,这样编译器至少可以知道TPartial&lt;Foo&gt; 以及它无法验证的其他内容:

mymethod<T extends Partial<Foo> & 
  { [K in keyof T]: K extends keyof Foo ? Foo[K] : never }
>(
    part: T
) {
    console.log(part);
    part.b?.toUpperCase(); // okay
}

这对你有用吗?

Playground link to code

【讨论】:

  • mymethod 有问题,访问方法内部的属性会导致编译时失败。
  • 是的,那行得通....必须弄清楚如何将那长长的代码块变成泛型类型...我还有一些其他问题...这可能会在内部通过强制转换来解决那个...
  • 您始终可以使用重载将调用签名与实现签名分开,例如mymethod&lt;T extends { [K in keyof T]: K extends keyof Foo ? Foo[K] : never }&gt;(part: T): void; 用于调用签名,然后mymethod(part: Partial&lt;Foo&gt;) {...} 用于实现
【解决方案2】:

@jcalz 的答案很棒,但不是我最终实现的,但它引导我到了那里。

type FooPart = Opaque<Partial<Foo>>;

Opaque,在这种情况下,来自type-fest 并强制你在创建它时定义你想要的类型,并且只允许在该类型中定义的键。

foo(): Foo { 
   return { a: 'test' } as FooPart;
}

在哪里

foo(): Foo { 
   return { a: 'test', d: 'not valid' } as FooPart;
}

会失败。

【讨论】:

    猜你喜欢
    • 2018-07-01
    • 1970-01-01
    • 2011-09-03
    • 2019-09-01
    • 1970-01-01
    • 1970-01-01
    • 2021-06-19
    • 2023-03-14
    • 1970-01-01
    相关资源
    最近更新 更多