-
-
Notifications
You must be signed in to change notification settings - Fork 569
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
Add SetRequiredDeep
type
#939
Add SetRequiredDeep
type
#939
Conversation
Can this feature be added on @sindresorhus how about you think? |
The problem with doing both shallow and deep in a single method is that the deep logic affects the performance even if you don't need deep. This was the case for |
1c4f238
to
e3c2874
Compare
Need to resolve all conversation |
Done |
e3c2874
to
40ad558
Compare
@sindresorhus @hugomartinet I'll have a look at your PR later today or tomorrow |
test-d/set-required-deep.ts
Outdated
|
||
// Set nested key to required | ||
declare const variation1: SetRequiredDeep<{a?: number; b?: {c?: string}}, 'b.c'>; | ||
expectType<{a?: number; b: {c: string}}>(variation1); |
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.
You haven't specified 'b' | 'b.c'
, therefore the expected behaviour should be expectType<{a?: number; b?: {c: string}}>(variation1);
(b is still optional)
Same below
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.
Thanks for this remark. I actually did this on purpose, in my mind I thought that this is what we want.
Since SetRequired<..., 'b.c'>
would mean that we want b.c
to always be defined, hence we also need b
to always be defined. Do you disagree ?
The reason is that I thought it would be fastidious when having nested objects with many levels. Writing SetRequired<..., 'a.b.${number}.c'>
is lighter than writing SetRequired<..., 'a' | 'a.b' | 'a.b.${number}' | 'a.b.${number}.c'>
.
What do you think ? If you're confident on making only the nested key required I can work on that again 😀
test-d/set-required-deep.ts
Outdated
|
||
// Set key inside array to required | ||
declare const variation7: SetRequiredDeep<{a?: Array<{b?: number}>}, `a.${number}.b`>; | ||
expectType<{a: Array<{b: number}>}>(variation7); |
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.
Similar here (and below), the only required path `a.${number}.b`
is specified, not 'a' | `a.${number}.b`
, therefore the expected behaviour should be expectType<{a?: Array<{b: number}>}>(variation7);
@hugomartinet I left my thoughts, I don't think it's ready because of a bug in the implementation (specified in my 2 comments where exactly), but I will leave it up to you (cc @sindresorhus) |
d76a9ca
to
51ec78e
Compare
@Beraliv I pushed a correction, I think I managed to take into account the expected behaviour you mentioned, thanks for pointing it out! |
@sindresorhus @Beraliv How should we proceed on this? |
@hugomartinet thank you for your patience! If it waits, I'll leave my comments by the end of the year (next 2 weeks). If not, proceed with what you have and I'll raise bugs later if I find anything unexpected |
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.
@hugomartinet The current implementation seems to fail in the following cases:
-
type T1 = SetRequiredDeep<{ a: 1; b: { c?: 1 } }, 'b.c'>; // ^? type T1 = { a: 1; b?: { c: 1 } }
Shouldn't make
b
optional.
-
type T1 = SetRequiredDeep<{ a: 1; readonly b: { c?: 1 } }, 'b.c'>; // ^? type T1 = { a: 1; b?: { c: 1 } }
Doesn't preserve the
readonly
modifier onb
.
-
type T1 = SetRequiredDeep<{ a: 1; readonly b: { c?: 1 } | number }, 'b.c'>; // ^? type T1 = { a: 1; b?: { c: 1 } }
Doesn't preserve the
| number
onb
.
-
type T1 = SetRequiredDeep<{ 0: 1; 1: { 2?: string } }, '1.2'>; // ^? type T1 = {0: 1; 1: {2?: string}}
Doesn't work with number keys.
@som-sm thank you for your summary of issues, this is exactly what I experienced when I was testing changes in October. @hugomartinet can you add unit tests for all scenarios I and @som-sm mentioned and fix those? Thank you |
@hugomartinet Looks like it's not working properly with index signatures: type Test = SetRequiredDeep<{[x: string]: unknown; a?: number}, 'a'>;
// ^? {[x: string]: unknown; [x: number]: unknown;} Should have returned And the implementation seems to have gotten really complicated, here's a much simpler implementation that passes all the existing tests and also works with index signatures. export type SetRequiredDeep<Type, KeyPaths extends Paths<Type>> =
Type extends NonRecursiveType
? Type
: SimplifyDeep<(
{[K in keyof Type as K extends (KeyPaths | StringToNumber<KeyPaths & string>) ? K : never]-?: Type[K]} extends infer RequiredPart
? keyof RequiredPart extends never ? unknown : RequiredPart
: never) & {
[K in keyof Type]: Extract<KeyPaths, `${K & (string | number)}.${string}`> extends never
? Type[K]
: SetRequiredDeep<Type[K], KeyPaths extends `${K & (string | number)}.${infer Rest extends Paths<Type[K]>}` ? Rest : never>
}>;
type StringToNumber<Keys extends string> = Keys extends `${infer N extends number}` ? N : never; @hugomartinet WDYT? Do you see any cases where the above implementation might fail? |
Looks good to me, thanks for the simplification |
@hugomartinet Great! I'll go ahead and update the PR then. Although, most likely the following bit can also be simplified further to just be the mapped type: {[K in keyof Type as K extends (KeyPaths | StringToNumber<KeyPaths & string>) ? K : never]-?: Type[K]} extends infer RequiredPart
? keyof RequiredPart extends never ? unknown : RequiredPart
: never Currently, if I replace the above bit with just the mapped type: {[K in keyof Type as K extends (KeyPaths | StringToNumber<KeyPaths & string>) ? K : never]-?: Type[K]} It fails to preserve the structure of arrays for some reason. So, Will have to spend more time to understand what's going wrong, but I guess it's fine for now. |
Thanks @som-sm! What are the next steps for this PR? |
@som-sm @hugomartinet I'm having a last look at the PR, please bear with me |
From test perspective, everything works as expected, great work @hugomartinet & @som-sm! |
Very interesting investigation, @som-sm! I tried to follow the path around
{} & {
b: {} & ({
c: 1;
} & {
c?: 1;
})[];
} All the next statements evaluated correctly: // { c: 1}[]
type B1 = SimplifyDeep<{} & Array<{ c: 1 }>>;
// {b: {c: 1}[]}
type B2 = SimplifyDeep<{
b: ({
c: 1;
} & {
c?: 1;
})[];
}>;
// {b: {c: 1}[]}
type B3 = SimplifyDeep<
{} & {
b: {} & ({
c: 1;
} & {
c?: 1;
})[];
}
>; But when I call |
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.
@hugomartinet @som-sm amazing work, thanks both of you for addressing the issues, it LGTM now!
@Beraliv Thanks for the detailed breakdown.
I think I've managed to find a simpler reproduction of this issue: import {Simplify} from "type-fest";
type T1 = Simplify<{} & {c: 1}[]>; // This works as expected
// ^? type T1 = {c: 1}[]
type T2 = Simplify<Pick<{}, never> & {c: 1}[]>; // This unwraps the array
// ^? type T2 = {[x: number]: {c: 1}; length:...
|
Also, simplified down the following conditional: {[K in keyof Type as K extends (KeyPaths | StringToNumber<KeyPaths & string>) ? K : never]-?: Type[K]} extends infer RequiredPart
? keyof RequiredPart extends never ? unknown : RequiredPart
: never to this: BaseType extends UnknownArray
? {}
: {[K in keyof BaseType as K extends (KeyPaths | StringToNumber<KeyPaths & string>) ? K : never]-?: BaseType[K]} I think this is much cleaner, refer this commit. NOTE: type T = SetRequiredDeep<{a: [string?]}, "a.0"> // doesn't remove the `?` modifier
// actual: {a: [string?]} // returns the same thing
// expected: {a: [string]} And, it would need more thought in cases like these: type T = SetRequiredDeep<{a: [string?, string?]}, "a.1"> // can't make `1` required, w/o making `0` required But, I guess this shouldn't be a blocker. |
@hugomartinet PR LGTM! @sindresorhus Shall we merge? |
@som-sm great, this looks even simpler now! |
It would be great if you could open an issue about this. |
Looks good. Nice work, everyone 🙏 |
Closes #796
This pull request adds a new
SetRequiredDeep
which, likeSetRequired
, allows to make one or several keys in a model required. It adds the possibility to select nested keys by specifying their path.Example