Fix: Behaviour of OptionalKeys when instantiated with primitives and arrays#414
Conversation
|
Hey @Beraliv, this is my first time contributing to this repository, please let me know if it's required to open an issue first. Thanks! |
|
Hey @som-sm! Thank you for taking your time and efforts to contribute to ts-essentials! I won't be able to have a look at your changes today, so I will come back to you tomorrow! Thank you for understanding in advance |
|
Quickly looking at the code, your changes look reasonable to me. I let the CI run on all TS versions to see if there are any regressions. I'll double-check the changes more thoroughly today to see if I missed anything. I'll ping you if needed |
|
Looks like something probably got fixed in From a quick test, the issue seems to be here: type Test<Type> = {
[Key in keyof Type]-?: [{ [Key2 in keyof Type]: never }[Key]];
};
type T = Test<[1, 2?]>;
// ^?Playground Link: https://tsplay.dev/w8Dd0w For v4.8 and below
Which is why the conditional type T1 = [1, 2?][1];
// ^? type T1 = 2 | undefinedWhereas for v4.9 and above
|
|
I've refactored the way we check for optional properties, this seems to be working fine for v4.8. export type OptionalKeys<Type> = Type extends object
? {
- [Key in keyof Type]-?: undefined extends { [Key2 in keyof Type]: Key2 }[Key] ? Key : never;
+ [Key in keyof Type]-?: Type extends Required<Pick<Type, Key>> ? never : Key;
}[keyof Type & (Type extends ReadonlyArray<any> ? number : keyof Type)]
: never; |
|
@som-sm thank you for your patience! I've reviewed your changes in detail and left 3 comments. The rest LGTM! Please address my comments and I'm happy to merge your changes |
|
@Beraliv Thanks for reviewing the PR. I've replied back to all the comments, please have a look. Also, there's scope for similar improvements in |
|
@Beraliv, while fixing the |
Beraliv
left a comment
There was a problem hiding this comment.
Nice usage of key remapping, overall LGTM, but a follow-up on tests
|
@som-sm I'm happy to merge your PR now, let me know what you think about my last comment with |
|
@Beraliv I think it's just fine to keep |
|
@som-sm no objections from me! Great work, merging your PR now! |
PR Checklist
Overview
Currently, the
OptionalKeystype doesn’t handle primitives and arrays as expected.Handling Primitives Correctly
Since
OptionalKeysis a homomorphic mapped type, so when it’s instantiated with a primitiveType, it simply returns backType. As a result, instantiatingOptionalKeyswithstringyieldsstring[keyof string], which isn’t the intended result.The fix here is straightforward, we simply bypass primitives by changing the conditional from
Type extends unknowntoType extends object. So, whenOptionalKeysis instantiated with primitives, the condition evaluates to false, returningneveras expected.Handling Arrays Correctly
Ideally,
OptionalKeys<[1, 2?]>should return"1", but instead, it returns a bunch of unintended things.Here's the existing code for reference:
Let's break down what happens when
OptionalKeysis instantiated with[1, 2?].{ [Key2 in keyof Type]: Key2 }) is homomorphic, it correctly preserves the tuple structure and evaluates to[Key2, Key2?](Note:Key2is insignificant here; could have simply beenneveras well). Therefore, the conditionalundefined extends ...evaluates to true only whenKeyis"1".[never, "1"].[never, "1"][keyof T]as this yields a bunch of unintended keys. Instead, if we index it with thenumbertype, we get the desired result of "1".The fix involves using
numberas the index for arrays instead ofkeyof Type.Additional Improvements & Tests:
Key2withneverin the inner mapped type for better readability. Using a specific type likeKey2can imply that its value is being utilised, which isn’t the case here.