英文:
Assigning a value from Partial of properties of an Object to that Object results in type error
问题
在处理 Angular 服务时,我遇到了一个问题,即将对象的部分属性分配给该对象的实例。
function foo<T>(input: Partial<T>, instance: T) {
// 不起作用:
for (const key in input) {
instance[key] = input[key];
}
// 不起作用:
for (const key in input) {
const i = input[key];
if (typeof i !== 'undefined') {
instance[key] = i;
}
}
// 起作用:
for (const key in input) {
instance[key] = input[key]!;
}
}
赋值操作会抛出类型错误。在第一个情况中,我得到了以下错误信息:
类型 'T[Extract<keyof T, string>] | undefined' 不能分配给类型 'T[Extract<keyof T, string>]。
'T[Extract<keyof T, string>]' 可以被实例化为一个可能与 'T[Extract<keyof T, string>] | undefined' 不相关的任意类型。(2322)
第二个情况结果为:
类型 'Partial<T>[Extract<keyof T, string>] & ({} | null)' 不能分配给类型 'T[Extract<keyof T, string>]。
'T[Extract<keyof T, string>]' 可以被实例化为一个可能与 'Partial<T>[Extract<keyof T, string>] & ({} | null)' 不相关的任意类型。(2322)
第三个情况可以工作,但我宁愿不在这里使用感叹号操作符。我已经没有其他解决办法了。
英文:
While working on an angular service I stumbled over a problem with assigning the properties of a Partial of an Object to an instance of that Object.
function foo<T>(input: Partial<T>, instance: T) {
// won't work:
for (const key in input) {
instance[key] = input[key];
}
// won't work:
for (const key in input) {
const i = input[key];
if (typeof i !== 'undefined') {
instance[key] = i;
}
}
// works:
for (const key in input) {
instance[key] = input[key]!;
}
}
The assignments throw a type error. In the first case, I get
Type 'T[Extract<keyof T, string>] | undefined' is not assignable to type 'T[Extract<keyof T, string>]'.
'T[Extract<keyof T, string>]' could be instantiated with an arbitrary type which could be unrelated to 'T[Extract<keyof T, string>] | undefined'.(2322)
second case results in
Type 'Partial<T>[Extract<keyof T, string>] & ({} | null)' is not assignable to type 'T[Extract<keyof T, string>]'.
'T[Extract<keyof T, string>]' could be instantiated with an arbitrary type which could be unrelated to 'Partial<T>[Extract<keyof T, string>] & ({} | null)'.(2322)
Third case works, but I'd rather not use the bang operator here. I am out of ideas how to solve this in any other way.
答案1
得分: 2
Sure, here's the translated content:
如果您有一个类型为 Partial<T>
的值,并且将其所有属性原样复制到类型为 T
的值中,那么您可能会发现自己错误地将 undefined
值复制到不希望出现的属性中(例如,如果未启用 --exactOptionalPropertyTypes
)。
因此,至少在不检查 undefined
的情况下,最好在这里出现错误。当然,即使在检查 undefined
时,您也会收到错误,因为 TypeScript 对于泛型类型的推理能力并不是很好。它不将 Partial<T>[K] & ({} | null)
视为可分配给 T[K]
,尽管我认为它是(我认为?我的泛型类型推理能力也可能不太好?)
由于 TypeScript 的类型系统并不完全稳定,始终有可能欺骗编译器允许您执行不安全的操作。从某种意义上说,您所做的任何旨在解决此问题的操作都不比使用非空断言运算符) 更安全,就像您在这里所做的一样:
function foo<T>(input: Partial<T>, instance: T) {
for (const key in input) {
if (typeof input[key] !== "undefined") {
instance[key] = input[key]!;
}
}
}
我认为这样做没有问题,因为它相对清晰地传达了您的意图:您检查了 undefined
,然后断言它不是 undefined
。
然而,如果您想避免类型断言,您可以利用 instance
从 T
扩展到 Partial<T>
的一些不稳定性。该语言认为对象在其属性类型上是协变的(请参阅https://stackoverflow.com/q/66410115/2887218),并且由于 T
的每个属性都可以分配给 Partial<T>
的相应属性,因此它将 T
视为可分配给 Partial<T>
。这给了您以下代码:
function foo<T>(input: Partial<T>, instance: T) {
const widenedInstance: Partial<T> = instance;
for (const key in input) {
if (typeof input[key] !== "undefined") {
widenedInstance[key] = input[key];
}
}
}
现在不会有错误。但实际上,它并不比您的其他代码更安全。对象实际上只在其只读属性上是协变的。一旦开始写入属性,协变就不安全了。上面的代码碰巧是安全的,因为我正在测试 undefined
,但如果没有测试 undefined
,或者如果我测试错误的方式,仍然不会有错误:
if (typeof input[key] === "undefined") { // 糟糕,没有错误
widenedInstance[key] = input[key];
}
因此,您必须小心。由于无论您做什么,都必须小心,所以无论您选择哪种方法,实际上都是主观的问题,或许还有第三种方法可能更好。
TypeScript 的不稳定性是有意而为之的,因为对 TypeScript 来说,完全稳定的类型系统简直是愚蠢的举动,并且会使 TypeScript 几乎无法使用,因此即使尝试通过编译器尽可能安全地验证代码(也许使用泛型来为键类型以及对象使用 T[K]
直接赋值而不是 T[keyof T]
,等等),它几乎肯定会在边缘情况下允许发生不安全的事情。最好采用对您有用的人性化代码...再次强调,这是主观的。
就像这里有一个潜在的第三种方法:
for (const key in input) {
const i: T[Extract<keyof T, string>] | undefined = input[key]
if (typeof i !== "undefined") {
instance[key] = i;
}
}
我在这里做的只是将您的 i
扩展到编译器要求的类型,以便在消除 undefined
后 instance[key] = i
起作用。这更好吗?🤷♂️
英文:
If you have a value of type Partial<T>
and just copy all of its properties to a value of type T
as-is, then you might find yourself erroneously copying an undefined
value into a property that doesn't expect it (e.g., if --exactOptionalPropertyTypes
is not enabled).
So it's probably good to get an error here, at least in the case where you don't check for undefined
. Of course you also get an error when you do check for undefined
, because TypeScript's ability to reason about generic types isn't very good. It doesn't see Partial<T>[K] & ({} | null)
as assignable to T[K]
, even though it is (I think? My ability to reason about generic types might also not be very good?)
Since TypeScript's type system isn't fully sound, it's always possible to fool the compiler into allowing you to do unsafe things. In some sense, anything you do that works around this is no safer than using the non-null assertion operator) as you've done here:
function foo<T>(input: Partial<T>, instance: T) {
for (const key in input) {
if (typeof input[key] !== "undefined") {
instance[key] = input[key]!;
}
}
}
I don't think there's anything wrong with this, since it conveys your intent fairly clearly to any future developer: you check for undefined and then assert that it's not undefined.
However, if you want to avoid a type assertion, you could take advantage of some unsoundness by widening instance
from T
to Partial<T>
. The language takes the position that objects are covariant in their property types (see https://stackoverflow.com/q/66410115/2887218 ) and since every property of T
is assignable to the corresponding property of Partial<T>
, then it sees T
as assignable to Partial<T>
. That gives you this code:
function foo<T>(input: Partial<T>, instance: T) {
const widenedInstance: Partial<T> = instance;
for (const key in input) {
if (typeof input[key] !== "undefined") {
widenedInstance[key] = input[key];
}
}
}
Now there's no error. But, it's not any safer than your other code, really. Objects are only really covariant in their read-only properties. As soon as you start writing to properties, then covariance is unsafe. The above code happens to be safe because I'm testing for undefined
, but I hadn't, or if I had made a mistake and tested the wrong way, there would still be no error:
if (typeof input[key] === "undefined") { // oops, no error
widenedInstance[key] = input[key];
}
So you have to be careful. And since you have to be careful here no matter what you do, it's essentially a matter of opinion whether either of the two approaches is better, or whether some third approach might be even better.
TypeScript's unsoundness is there intentionally, because a fully sound type system for TypeScript is "simply a fool's errand" and would TypeScript almost unusable, so even if you try to rewrite the code to be as verified as safe as possible by the compiler (maybe using generics for the key type as well as the object so that you are assigning T[K]
directly instead of T[keyof T]
, etc) it will almost certainly allow unsafe things to happen in edge cases. You might as well go for ergonomic code that works for you... which again, is subjective.
Like, here's a potential third approach:
for (const key in input) {
const i: T[Extract<keyof T, string>] | undefined = input[key]
if (typeof i !== "undefined") {
instance[key] = i;
}
}
All I've done here is widen your i
to the type the compiler requires for instance[key] = i
to work once undefined
is eliminated. Is that better? 🤷♂️
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论