-
Notifications
You must be signed in to change notification settings - Fork 630
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(collections): ensure pick
doesn't generate missing properties as undefined
#5926
Conversation
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #5926 +/- ##
==========================================
+ Coverage 96.25% 96.32% +0.06%
==========================================
Files 489 483 -6
Lines 39457 39405 -52
Branches 5824 5839 +15
==========================================
- Hits 37980 37955 -25
+ Misses 1433 1408 -25
+ Partials 44 42 -2 ☔ View full report in Codecov by Sentry. |
Oops, for now it should respect non-own properties as it used to. |
collections/pick_test.ts
Outdated
"toString", | ||
]); | ||
|
||
assertEquals(picked, { toString: Object.prototype.toString }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I personally don't think this is intentionally in this way.. (The check with Object.hasOwn
meets the users' expectation better in my view)
@lambdalisue What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Considering the original purpose, I hadn't envisioned a use case where toString
would be picked. Therefore, I think @kt3k's suggestion is a good one. However, since the implementation currently references obj[k]
, changing it to Object.hasOwn
would be a breaking change. Additionally, in terms of type expression, it's written as K in keyof T
, which means it allows something like toString
(the following code does not result in a type error).
import { pick } from "jsr:@std/collections/pick";
import { omit } from "jsr:@std/collections/omit";
class A {
constructor(public a: string, public b: string, public c: string) {}
foo(): string {
return "hello";
}
}
const obj = new A("a", "b", "c");
const p = pick(obj, ["a", "c", "foo"]);
const o = omit(obj, ["b"]);
console.log(obj.foo());
console.log(p.foo());
console.log(o.foo());
This might be beyond the scope of this PR, but I realized that o.foo()
does not work in the code above. Thus, currently there is an inconsistency between pick
and omit
. Given this situation, we need to choose between the following options:
- Restrict the targets of
pick
andomit
toObject.hasOwn
(and adjust the type so it expresses this correctly). - Fix the implementation of
omit
so thato.foo()
does not result in a runtime error.
Personally, I prefer option 1, but I’m not sure how to express this with types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually agree with @kt3k, the test case is just to avoid introducing accidental breaking change in the meantime.
I appreciate all the consideration given by @lambdalisue. Especially, the inconsistency between pick
and omit
is one of the reasons I proposed #5927; the most beautiful solution I think is to only operate on own properties and copy the prototype to the result, which is kind of a merge of option 1 and 2. Try replacing the imports with the following code and rerun your example code:
const pick = <T extends object, K extends keyof T>(
obj: Readonly<T>,
keys: readonly K[],
): Pick<T, K> => {
const result = Object.create(Object.getPrototypeOf(obj))
for (const key of keys) {
const descriptor = Object.getOwnPropertyDescriptor(obj, key)
if (descriptor) Object.defineProperty(result, key, descriptor)
}
if (!Object.isExtensible(obj)) Object.preventExtensions(result)
return result
}
const omit = <T extends object, K extends keyof T>(
obj: Readonly<T>,
keys: readonly K[],
): Omit<T, K> => {
const result = Object.create(Object.getPrototypeOf(obj))
const excludes = new Set(keys)
for (const key of Reflect.ownKeys(obj)) {
if (!excludes.has(key as K)) {
const descriptor = Object.getOwnPropertyDescriptor(obj, key)!
Object.defineProperty(result, key, descriptor)
}
}
if (!Object.isExtensible(obj)) Object.preventExtensions(result)
return result
}
But yes, the parameter types will be looser and the return types will be stricter than desired this way, though still sound.
Anyways it's beyond the scope, so I suggest moving to another issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, ok. The current check (key in obj
) works good for the case of the above class A
example (foo
method should be picked in that case).
I'm now in favor of landing this fix.
However, I'm still not sure we should include this particular example (pick(obj, ["toString"])
) as a test case, as this causes type error if we don't type cast obj
to any. I think we can consider this particular behavior something like undefined/not decided behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about just removing this test case and revisit later when someone comes up with something better?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not opposed to it, but just curious, is the concept of “undefined behavior” common in the standard library? If we decide to make this UB, there has to be an explicit notice in the JSDoc comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not common to make something explicitly undefined behavior in the standard library, but I think we are not ready to decide about this behavior, and also this looks out of scope of this PR.
In my view, in the below example, pick
ing foo
looks fine, but pick
ing toString
looks strange as it doesn't match its type (it's actually causes a type error).
import { pick } from "@std/collections";
class A {
foo() {
}
bar() {
}
}
const a = new A();
pick(a, ["foo"]);
pick(a, ["toString"]);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Thanks!
I've created an issue for |
Fixes #5922.