-
Notifications
You must be signed in to change notification settings - Fork 127
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
feat: add hasProp
, hasPropSatisfying
, and isEqual
#515
base: master
Are you sure you want to change the base?
Conversation
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit e4023c9:
|
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## master #515 +/- ##
==========================================
+ Coverage 98.41% 98.54% +0.12%
==========================================
Files 127 138 +11
Lines 7954 9271 +1317
Branches 724 782 +58
==========================================
+ Hits 7828 9136 +1308
- Misses 123 132 +9
Partials 3 3 ☔ View full report in Codecov by Sentry. |
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 think it's unlikely that people would consider an array as having a prop at an index. We already offer
hasAtLeast
to narrow arrays so that accessing a specific prop would result in a non-undefined value. Handling arrays in these functions complicates their type considerably and might introduce edge-cases and bugs that would be hard to find and fix. Let's keephasProp
andhasPropSatisfying
for proper objects just as pick, omit, and prop are. -
Handling unions specifically is also beyond what the utility functions should handle. We don't handle these issues with
prop
,pick
, oromit
either. If this is to handle discriminated unions, it's usually recommended to have a pivot prop that would allow typescript to align the types. I personally encountered issues in the past with trying to rely on prop existence in order to discriminate a union. Let's keep this simple for now, and we can always go back to this and expand it if we see that the simple solution isn't working for people.
src/_objectProps.ts
Outdated
|
||
export type AllUnionKeys<Union> = Union extends Union ? keyof Union : never; | ||
|
||
type MakeKeyRequired<Obj, Prop extends keyof Obj> = Obj & { |
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.
To prevent order from being an issue here you'd need to remove the props from Obj first.
type MakeKeyRequired<Obj, Prop extends keyof Obj> = Obj & { | |
type MakeKeyRequired<Obj, Prop extends keyof Obj> = Omit<Obj, Prop> & { |
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.
This was my original approach but ran into issues with the guard types. Typescript was thinking that this type was no longer assignable to the original.
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.
Spent a little time trying to add this in and ran into the same issue. I'd need to do some more research to figure out exactly why, but TS gives this error in the hasProp
implementation file:
A type predicate's type must be assignable to its parameter's type.
Good callout. I didn't realize that was there! I only added the type handling because it the implementation technically supports it without extra effort.
I'm not sure I understand the perspective that this is beyond the scope of these helpers. I usually agree with implementing less complete feature sets and filling it out over time, but since we already have a more complete feature set it doesn't make sense to me to pare it down now. I agree that But since these helpers are type guards, the whole point is to take a wider type and narrow it. |
Those are good points about it being a type-guard requiring extra care about the types. I think the best thing would be to ensure that there are tests covering these concerns so that we can't accidentally break the code for these cases without even knowing. None of the tests break at the moment when I remove the |
In the issue, there was some discussion about the behavior of the |
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.
undefined
is not special when it comes to prop values, it should be treated as a valid value and that should be represented both in the types and the runtime behaviour of hasProp.
See my comment inline in the test file.
export type AllUnionKeys<Union> = Union extends Union ? keyof Union : never; | ||
|
||
// Used to ensure the type asserted by a type guard is recognized as a subtype of the original type. | ||
export type EnsureExtends<T, U> = U extends T ? U : Simplify<T & U>; |
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.
Do we need to simplify here? Obviously, all types would benefit from being simplified, but I'm not sure when it's actually useful. Although this doesn't affect runtime performance, Typescript itself can become sluggish when too much recursive and iterative inference is expected from it.
type MakeKeyRequired<Obj, Prop extends keyof Obj> = Omit<Obj, Prop> & { | ||
// We need to both remove the optional modifier and exclude undefined. | ||
// This is because the `-?` modifier will only exclude undefined from the type if the property is optional, | ||
// which makes the behavior of the type inconsistent. | ||
// More info about this can be found here: https://github.com/microsoft/TypeScript/issues/31025 | ||
// So excluding `undefined` explicitly also does this for required properties that include undefined. | ||
[key in Prop]-?: Exclude<Obj[key], undefined>; | ||
}; | ||
|
||
type UnionMembersWithProp<Obj, Prop extends keyof Obj> = Extract< | ||
Obj, | ||
{ [key in Prop]?: unknown } | ||
>; | ||
|
||
export type WithRequiredProp<Obj, Prop extends AllUnionKeys<Obj>> = Simplify< | ||
MakeKeyRequired<UnionMembersWithProp<Obj, Prop>, Prop> | ||
>; |
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.
Let's pull this out into the type-fest folder and copy:
https://github.com/sindresorhus/type-fest/blob/main/source/set-required.d.ts
it("should return false when a prop is present on the object but is undefined", () => { | ||
const obj = { b: undefined }; | ||
expect(hasProp(obj, "b")).toBe(false); | ||
}); |
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.
This is incorrect and relies on a more permissive type definition. When the typescript project uses exactOptionalPropertyTypes
an undefined
value is strictly different than an optional prop not being present. This is also how it works during runtime.
console.log('a' in { a: undefined }); // "true"
console.log('a' in {}); // "false"
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.
This behavior was an intentional trade-off. I can play around a bit more since it's been awhile, but I remember something at the type level couldn't reliably distinguish between the two.
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.
Check out type-fest types for some of your utility types under _objectProps, they most likely can solve most of those issues.
hasProp should work as closely as possible to "prop" in <obj>
otherwise some users would either misunderstand it's semantics and run into problems in runtime, or need to build their own function for the behaviour they need.
hasPropSatisfying(obj, prop, isDefined.strict)
can provide the functionality you define for hashProp
here, but the opposite is not possible.
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'll check it out and see what I can do. This is the issue I reference elsewhere in the PR that lead me to this decision. microsoft/TypeScript#31025
Resolves #461
Summary
Adds three new helpers:
isEqual
does a shallow,Object.is
equality comparison and acts as a type guardhasProp
checks if a given object has a property and that it's definedhasPropSatisfying
checks if a given object has property, that it's defined, and that the provided predicate/type guard returns true.Design considerations
isEqual
I originally thought to just do an
===
comparison, but I thoughtObject.is
made more sense since it's more robust to values likeNaN
.hasProp
My original plan was to only check for the presence of a property and allow checks like
hasProp({ a: undefined }, 'a')
to return true.However, Typescript removes
undefined
from the possible values when removing the optionality of the prop (related github issue). The linked thread has some workarounds, but I wasn't able to get any of them to work in a general enough way for this helper.This means that the narrowed type resulting from the type guard couldn't be accurate if we returned true for present keys with
undefined
as the value.I think returning false when the value is
undefined
is a reasonable trade-off to make to keep the types completely accurate.hasPropSatisifying
Same considerations as
hasProp
, but the type accuracy issue also extends to the type of the argument passed to the predicate.Potential future improvements
Lodash and Ramda both have the ability to check for properties at a given path.
has
supports it directlyhasPath
helperAs a future improvement, we could conceivably add a couple
hasPropAtPath
andhasPropAtPathSatisfying
helpers.Make sure that you:
src/index.ts
mapping.md
We use semantic PR titles to automate the release process!
https://conventionalcommits.org
PRs should be titled following using the format:
< TYPE >(< scope >)?: description
Available Types:
feat
: new functions, and changes to a function's type that would impact users.fix
: changes to the runtime behavior of an existing function, or refinements to it's type that shouldn't impact most users.perf
: changes to function implementations that improve a functions runtime performance.refactor
: changes to function implementations that are neitherfix
norperf
test
: tests-only changes (transparent to users of the function).docs
: changes to the documentation of a function or the documentation site.build
,ci
,style
,chore
, andrevert
: are only relevant for the internals of the library.For scope put the name of the function you are working on (either new or
existing).