Skip to content
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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export type {InvariantOf} from './source/invariant-of';
export type {SetOptional} from './source/set-optional';
export type {SetReadonly} from './source/set-readonly';
export type {SetRequired} from './source/set-required';
export type {SetRequiredDeep} from './source/set-required-deep';
export type {SetNonNullable} from './source/set-non-nullable';
export type {ValueOf} from './source/value-of';
export type {AsyncReturnType} from './source/async-return-type';
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ Click the type names for complete docs.
- [`SetOptional`](source/set-optional.d.ts) - Create a type that makes the given keys optional.
- [`SetReadonly`](source/set-readonly.d.ts) - Create a type that makes the given keys readonly.
- [`SetRequired`](source/set-required.d.ts) - Create a type that makes the given keys required.
- [`SetRequiredDeep`](source/set-required-deep.d.ts) - Like `SetRequired` except it selects the keys deeply.
- [`SetNonNullable`](source/set-non-nullable.d.ts) - Create a type that makes the given keys non-nullable.
- [`ValueOf`](source/value-of.d.ts) - Create a union of the given object's values, and optionally specify which keys to get the values from.
- [`ConditionalKeys`](source/conditional-keys.d.ts) - Extract keys from a shape where values extend the given `Condition` type.
Expand Down
45 changes: 45 additions & 0 deletions source/set-required-deep.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type {OmitDeep} from './omit-deep';
import type {Paths} from './paths';
import type {PickDeep} from './pick-deep';
import type {RequiredDeep} from './required-deep';
import type {SimplifyDeep} from './simplify-deep';

/**
Create a type that makes the given keys required. You can specifiy deeply nested key paths. The remaining keys are kept as is.

Use-case: You want to define as required only a some nested keys inside a model.

@example
```
import type {SetRequiredDeep} from 'type-fest';

type Foo = {
a?: number;
b?: string;
hugomartinet marked this conversation as resolved.
Show resolved Hide resolved
array?: {
c?: number
}[]
}

type SomeRequiredDeep = SetRequiredDeep<Foo, 'a' | 'array.c'>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
type SomeRequiredDeep = SetRequiredDeep<Foo, 'a' | 'array.c'>;
type SomeRequiredDeep = SetRequiredDeep<Foo, 'a' | 'array.${number}.c'>;

I kinda forgot how it's should be. You can add the example in test to ensure it is worked.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kinda forgot how it's should be. You can add the example in test to ensure it is worked.

This doesn't seem to be resolved @hugomartinet

// type SomeRequiredDeep = {
// a: number; // Is now required
// b?: string;
// array: {
// c: number // Is now required
// }[]
// }
```

@category Object
*/
export type SetRequiredDeep<BaseType, KeyPaths extends Paths<BaseType>> =
Copy link

@Beraliv Beraliv Aug 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm playing with this solution using the following example:

type Teacher = { address?: { postcode?: string; city?: string } };
type P1 = SetRequiredDeep<Teacher, 'address' | 'address.city'>;
//      ^?

This gives me:

type P1 = {
    address: {
        postcode: string;
        city: string;
    };
} | {
    address: {
        postcode: string;
        city: string;
    };
}

Is the result expected?

To elaborate:

  1. Why is the postcode required?
  2. Why is the result object a union? Are there any plans to merge a union into a single object?

Copy link
Collaborator

@Emiyaaaaa Emiyaaaaa Aug 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, similar to PickDeep and OmitDeep. result should be

type P1 = { 
  address: { 
    postcode?: string; 
    city: string; 
  }; 
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I think my current implementation could not work for this, since I use RequiredDeep and RequiredDeep makes all child keys of an object required as well.

// `extends unknown` is always going to be the case and is used to convert any union into a [distributive conditional type](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#distributive-conditional-types).
BaseType extends unknown
? SimplifyDeep<
// Pick just the keys that are optional from the base type.
OmitDeep<BaseType, KeyPaths> &
// Pick the keys that should be required from the base type and make them required.
RequiredDeep<PickDeep<BaseType, KeyPaths>>
>
: never;
35 changes: 35 additions & 0 deletions test-d/set-required-deep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {expectAssignable, expectNotAssignable, expectType} from 'tsd';
import type {MergeDeep, OverrideProperties, SetRequired, SetRequiredDeep, Simplify, SimplifyDeep} from '../index';

// Update an optional nested key to required.
declare const variation1: SetRequiredDeep<{a?: number; b?: {c?: string}}, 'b.c'>;
expectType<{a?: number; b: {c: string}}>(variation1);

hugomartinet marked this conversation as resolved.
Show resolved Hide resolved
// Update a root key to required.
declare const variation2: SetRequiredDeep<{a?: number; b?: {c?: string}}, 'a'>;
expectType<{a: number; b?: {c?: string}}>(variation2);
Copy link

@Beraliv Beraliv Aug 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth adding a test case with multiple paths (i.e. BaseType is not a union)?

declare const variation2: SetRequiredDeep<{a?: number; b?: {c?: string}}, 'a' | 'b'>;
expectType<{a: number; b: {c?: string}}>(variation2);


// Keeps required key as required
declare const variation3: SetRequiredDeep<{a: number; b?: {c?: string}}, 'a'>;
expectType<{a: number; b?: {c?: string}}>(variation3);

// Update optional keys to required in a union.
declare const variation4: SetRequiredDeep<{a?: '1'; b?: {c?: boolean}} | {a?: '2'; b?: {c?: boolean}}, 'a' | 'b.c'>;
expectType<{a: '1'; b: {c: boolean}} | {a: '2'; b: {c: boolean}}>(variation4);
hugomartinet marked this conversation as resolved.
Show resolved Hide resolved

hugomartinet marked this conversation as resolved.
Show resolved Hide resolved
// Set key inside array to required
declare const variation5: SetRequiredDeep<{a?: number; array?: Array<{b?: number}>}, 'array'>;
expectType<{a?: number; array: Array<{b: number}>}>(variation5);

// Set specific key inside array to required
expectAssignable<SetRequiredDeep<{a?: number; array?: Array<{b?: number}>}, 'array.0.b'>>({a: 1, array: [{b: 2}]});
expectNotAssignable<SetRequiredDeep<{a?: number; array?: Array<{b?: number}>}, 'array.0.b'>>({array: [{}]});

// Set only specified keys inside array to required
expectAssignable<SetRequiredDeep<{a?: number; array?: Array<{b?: number; c?: string}>}, `array.${number}.b`>>({array: [{b: 4}]});
expectNotAssignable<SetRequiredDeep<{a?: number; array?: Array<{b?: number; c?: string}>}, `array.${number}.b`>>({array: [{}]});

// Set specific key inside specific array item to required
expectAssignable<SetRequiredDeep<{a?: number; array?: [{b?: number}, {c?: string}]}, 'array.1.c'>>({array: [{}, {c: 'foo'}]});
expectNotAssignable<SetRequiredDeep<{a?: number; array?: [{b?: number}, {c?: string}]}, 'array.1.c'>>({array: [{b: 2}, {}]});