Skip to content

Add SetNonNullableDeep type #1117

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

Merged
merged 2 commits into from
May 6, 2025
Merged
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 @@ -47,6 +47,7 @@ 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 {SetNonNullableDeep} from './source/set-non-nullable-deep';
export type {ValueOf} from './source/value-of';
export type {AsyncReturnType} from './source/async-return-type';
export type {ConditionalExcept} from './source/conditional-except';
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ Click the type names for complete docs.
- [`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.
- [`SetNonNullableDeep`](source/set-non-nullable-deep.d.ts) - Create a type that makes the specified keys non-nullable (removes `null` and `undefined`), supports deeply nested key paths, and leaves all other keys unchanged.
- [`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.
- [`ConditionalPick`](source/conditional-pick.d.ts) - Like `Pick` except it selects properties from a shape where the values extend the given `Condition` type.
Expand Down
83 changes: 83 additions & 0 deletions source/set-non-nullable-deep.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type {NonRecursiveType, StringToNumber} from './internal';
import type {Paths} from './paths';
import type {SetNonNullable} from './set-non-nullable';
import type {Simplify} from './simplify';
import type {UnionToTuple} from './union-to-tuple';
import type {UnknownArray} from './unknown-array';

/**
Create a type that makes the specified keys non-nullable (removes `null` and `undefined`), supports deeply nested key paths, and leaves all other keys unchanged.

NOTE: Optional modifiers (`?`) are not removed from properties. For example, `SetNonNullableDeep<{foo?: string | null | undefined}, 'foo'>` will result in `{foo?: string}`.

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

type User = {
name: string;
address: {
city: string | undefined;
street?: string | null;
};
contact: {
email?: string | null | undefined;
phone: string | undefined;
};
};

type UpdatedUser = SetNonNullableDeep<User, 'address.street' | 'contact.email' | 'contact.phone'>;
//=> {
// name: string;
// address: {
// city: string | undefined;
// street?: string;
// };
// contact: {
// email?: string;
// phone: string;
// };
// };
```

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

// Set specific indices in an array to be non-nullable.
type ArrayExample1 = SetNonNullableDeep<{a: [number | null, number | null, number | undefined]}, 'a.1' | 'a.2'>;
//=> {a: [number | null, number, number]}

// Optional modifier (`?`) is not removed.
type ArrayExample2 = SetNonNullableDeep<{a: [(number | null)?, (number | null)?]}, 'a.1'>;
//=> {a: [(number | null)?, number?]}
```

@category Object
*/
export type SetNonNullableDeep<BaseType, KeyPaths extends Paths<BaseType>> =
SetNonNullableDeepHelper<BaseType, UnionToTuple<KeyPaths>>;

/**
Internal helper for {@link SetNonNullableDeep}.

Recursively transforms the `BaseType` by applying {@link SetNonNullableDeepSinglePath} for each path in `KeyPathsTuple`.
*/
type SetNonNullableDeepHelper<BaseType, KeyPathsTuple extends UnknownArray> =
KeyPathsTuple extends [infer KeyPath, ...infer RestPaths]
? SetNonNullableDeepHelper<SetNonNullableDeepSinglePath<BaseType, KeyPath>, RestPaths>
: BaseType;

/**
Makes a single path non-nullable in `BaseType`.
*/
type SetNonNullableDeepSinglePath<BaseType, KeyPath> =
BaseType extends NonRecursiveType | ReadonlySet<unknown> | ReadonlyMap<unknown, unknown> // Also distributes `BaseType`
? BaseType
: KeyPath extends `${infer Property}.${infer RestPath}`
? {
[Key in keyof BaseType]: Property extends `${Key & (string | number)}`
? SetNonNullableDeepSinglePath<BaseType[Key], RestPath>
: BaseType[Key];
}
: Simplify<SetNonNullable<BaseType, (KeyPath | StringToNumber<KeyPath & string>) & keyof BaseType>>;
158 changes: 158 additions & 0 deletions test-d/set-non-nullable-deep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import {expectType} from 'tsd';
import type {SetNonNullableDeep} from '../source/set-non-nullable-deep';

expectType<{a: number}>({} as SetNonNullableDeep<{a: number | null}, 'a'>);
expectType<{a: number}>({} as SetNonNullableDeep<{a: number | undefined}, 'a'>);
expectType<{a: number}>({} as SetNonNullableDeep<{a: number | null | undefined}, 'a'>);
expectType<{a?: number}>({} as SetNonNullableDeep<{a?: number | null}, 'a'>);
expectType<{a?: number}>({} as SetNonNullableDeep<{a?: number | undefined}, 'a'>);
expectType<{a?: number}>({} as SetNonNullableDeep<{a?: number | null | undefined}, 'a'>);

expectType<{a: number; b: {c: string}}>({} as SetNonNullableDeep<{a: number; b: {c: string | null}}, 'b.c'>);
expectType<{a: number; b: {c: string}}>({} as SetNonNullableDeep<{a: number; b: {c: string | undefined}}, 'b.c'>);
expectType<{a: number; b: {c: string}}>({} as SetNonNullableDeep<{a: number; b: {c: string | null | undefined}}, 'b.c'>);
expectType<{a: number; b: {c?: string}}>({} as SetNonNullableDeep<{a: number; b: {c?: string | null}}, 'b.c'>);
expectType<{a: number; b: {c?: string}}>({} as SetNonNullableDeep<{a: number; b: {c?: string | undefined}}, 'b.c'>);
expectType<{a: number; b: {c?: string}}>({} as SetNonNullableDeep<{a: number; b: {c?: string | null | undefined}}, 'b.c'>);

// Becomes `never` when the value is only `null` or `undefined`
expectType<{a: never}>({} as SetNonNullableDeep<{a: null | undefined}, 'a'>);
expectType<{a?: never}>({} as SetNonNullableDeep<{a?: null | undefined}, 'a'>);

// Ignores keys that are already non-nullable
expectType<{a: number}>({} as SetNonNullableDeep<{a: number}, 'a'>);
expectType<{readonly a?: number}>({} as SetNonNullableDeep<{readonly a?: number}, 'a'>);
expectType<{a?: number; b: {c: string}}>({} as SetNonNullableDeep<{a?: number; b: {c: string}}, 'b.c'>);
expectType<{a: number; readonly b?: {c?: string}}>({} as SetNonNullableDeep<{a: number; readonly b?: {c?: string}}, 'b.c'>);

// Removes nullables only from the specified keys, nullables in other paths are preserved
expectType<{a: number; b: {c: string | null | undefined} | null | undefined}>(
{} as SetNonNullableDeep<{a: number | null | undefined; b: {c: string | null | undefined} | null | undefined}, 'a'>,
);
expectType<{a: number | null | undefined; b: {c: string | null | undefined}}>(
{} as SetNonNullableDeep<{a: number | null | undefined; b: {c: string | null | undefined} | null | undefined}, 'b'>,
);
expectType<{a: number | null | undefined; b: {c: string} | null | undefined}>(
{} as SetNonNullableDeep<{a: number | null | undefined; b: {c: string | null | undefined} | null | undefined}, 'b.c'>,
);
expectType<{a: {b: string}}>({} as SetNonNullableDeep<{a: {b: string | null} | null | undefined}, 'a' | 'a.b'>);
expectType<{a: {b: {c: string}} | null | undefined}>({} as SetNonNullableDeep<{a: {b: {c: string | undefined} | null} | null | undefined}, 'a.b' | 'a.b.c'>);
expectType<{a: {b: {c: string} | null}}>({} as SetNonNullableDeep<{a: {b: {c: string | undefined} | null} | null | undefined}, 'a' | 'a.b.c'>);
expectType<{a: {b?: {c: {d?: {e: number | string}; f: {g: boolean}} | undefined} | null; h: number | null}}>(
{} as SetNonNullableDeep<
{a: {b?: {c: {d?: {e: number | string} | null; f: {g: boolean | null}} | undefined} | null; h: number | null} | undefined},
'a' | 'a.b.c.d' | 'a.b.c.f.g'
>,
);

// Unions
expectType<{a: string} | {a?: number} | {a: boolean}>({} as SetNonNullableDeep<{a: string | null} | {a?: number | undefined} | {a: boolean | null | undefined}, 'a'>);
expectType<{a: string; b: number}>({} as SetNonNullableDeep<{a: string | null; b: number | undefined}, 'a' | 'b'>);
expectType<{a: string; b: {c: {d: {e: number}} | null} | undefined}>(
{} as SetNonNullableDeep<{a: string | null; b: {c: {d: {e: number | undefined}} | null} | undefined}, 'a' | 'b.c.d.e'>,
);
expectType<{a: {b: string | null}} | {a?: number; b: {c?: string}}>(
{} as SetNonNullableDeep<{a: {b: string | null} | undefined} | {a?: number | undefined; b: {c?: string}}, 'a' | 'b.c'>,
);
expectType<{a: string; b: {c?: {d: string} | undefined; f?: number}} | {a: {b: number}; c: never; d?: undefined}>(
{} as SetNonNullableDeep<
{a: string; b: {c?: {d: string | null} | undefined; f?: number} | null} | {a: {b: number | null}; c: null; d?: undefined},
'b' | 'b.c.d' | 'a.b' | 'c'
>,
);
expectType<{a: 1; b: {c: 2}; d?: {e?: {f?: 2}; g?: 3}}>(
{} as SetNonNullableDeep<{a: 1 | null; b: {c: 2 | null}; d?: {e?: {f?: 2 | null}; g?: 3}}, 'a' | 'b' | 'b.c' | 'd.e.f' | 'd.g'>,
);
expectType<{a: {b: string} | {c: string} | {b: {c: string | null}}}>(
{} as SetNonNullableDeep<{a: {b: string | null} | {c: string} | {b: {c: string | null} | undefined}}, 'a.b'>,
);

// Preserves non-nullable values when they are in a union with objects
expectType<{a?: {b: string} | number}>({} as SetNonNullableDeep<{a?: {b: string} | number | null | undefined}, 'a'>);
expectType<{a: {b: Array<{c?: number | null}> | number}}>({} as SetNonNullableDeep<{a: {b: Array<{c?: number | null}> | number | null}}, 'a.b'>);
expectType<{a: {b: string | number} | number}>({} as SetNonNullableDeep<{a: {b: string | number | null} | number | null | undefined}, 'a' | 'a.b'>);
expectType<{a: number; b: {c: number | null} | {d: string}}>({} as SetNonNullableDeep<{a: number; b: {c: number | null} | {d: string | undefined} | null}, 'b' | 'b.d'>);

// Preserves `readonly` modifier
expectType<{a: string; b: {readonly c: number}}>({} as SetNonNullableDeep<{a: string; b: {readonly c: number | null}}, 'b.c'>);
expectType<{readonly a: string; readonly b: {c: number | null}}>({} as SetNonNullableDeep<{readonly a: string | null; readonly b: {c: number | null} | null}, 'a' | 'b'>);
expectType<{readonly a: string; readonly b: {readonly c: number}}>({} as SetNonNullableDeep<{readonly a: string | null; readonly b: {readonly c: number | null} | null}, 'a' | 'b' | 'b.c'>);

// Number keys
expectType<{0: 1; 1: {2?: string}}>({} as SetNonNullableDeep<{0: 1; 1: {2?: string | null}}, '1.2'>);
expectType<{0: 1; 1?: {2: string | null}}>({} as SetNonNullableDeep<{0: 1 | null; 1?: {2: string | null} | undefined}, 0 | 1>);

// Number keys containing dots
// NOTE: Passing "1.2" instead of 1.2 will treat it as a path instead of a key
expectType<{1.2?: string; 1?: {2?: string | null} | null}>({} as SetNonNullableDeep<{1.2?: string | null; 1?: {2?: string | null} | null}, 1.2>);
expectType<{1.2?: string | null; 1?: {2?: string} | null}>({} as SetNonNullableDeep<{1.2?: string | null; 1?: {2?: string | null} | null}, '1.2'>);
expectType<{1.2?: string; 1?: {2?: string} | undefined}>({} as SetNonNullableDeep<{1.2?: string | undefined; 1?: {2?: string | undefined} | undefined}, 1.2 | '1.2'>);

// Index signatures
expectType<{[x: string]: any; a: number; b: {c: number}}>({} as SetNonNullableDeep<{[x: string]: any; a: number | null; b: {c: number | null}}, 'a' | 'b.c'>);

// Works with `KeyPaths` containing template literals
expectType<{a: number | null; b: {c: number} | {d: number} | null | undefined}>({} as SetNonNullableDeep<{a: number | null; b: {c: number | null} | {d: number | undefined} | null | undefined}, `b.${'c' | 'd'}`>);
expectType<{a: number; b: null | {readonly c: {1: number[]} | undefined} | {d: {1: number[]} | null}}>({} as SetNonNullableDeep<
{a: number | undefined; b: null | {readonly c: {1: number[] | undefined} | undefined} | {d: {1: number[] | undefined} | null}}, 'a' | `b.${'c' | 'd'}.1`
>);

// Non recursive types
expectType<{a: {b: never} | Set<number | null>}>({} as SetNonNullableDeep<{a: {b: null} | Set<number | null>}, 'a.b'>);
expectType<{a: {b: {c: string} | Map<string, string>}}>({} as SetNonNullableDeep<{a: {b: {c: string} | Map<string, string> | null}}, 'a.b'>);

// === Arrays ===
expectType<[string, number | null, boolean]>({} as SetNonNullableDeep<[string | null, number | null, boolean | undefined], '0' | '2'>);
expectType<{a?: [string, number, (boolean | null)?] | null}>({} as SetNonNullableDeep<{a?: [string | undefined, number | undefined, (boolean | null)?] | null}, 'a.0' | 'a.1'>);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Something really strange is happening here. If I remove the test on line 106, some tests in set-required-deep.ts start failing—tests that are completely unrelated to this. And there's no changes in SetRequiredDeep related files.

And this doesn't just fail locally, it also fails in CI.

@sindresorhus @Emiyaaaaa Do you have any idea why this could be happening?

weird-compressed.mov

Copy link
Owner

Choose a reason for hiding this comment

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

No idea. Probably some compiler quirk.

The minimum. Any more simplifications of this and it triggers the issue.

expectType<{a?: [string]}>({} as SetNonNullableDeep<{a?: [string]}, 'a.0'>);

expectType<{a?: [string | null, number?, boolean?] | null}>({} as SetNonNullableDeep<{a?: [string | null, (number | undefined)?, (boolean | null)?] | null}, 'a.1' | 'a.2'>);
expectType<{a: readonly [string, number, (boolean | null)?]}>({} as SetNonNullableDeep<{a: readonly [string, number | undefined, (boolean | null)?]}, 'a.1'>);
expectType<{readonly a: [string, number | null | undefined, boolean, ...Array<number | null>]}>(
{} as SetNonNullableDeep<{readonly a: [string | null, number | null | undefined, boolean | undefined, ...Array<number | null>]}, 'a.0' | 'a.2'>,
);
expectType<{readonly a?: [string, number, boolean, ...Array<string | null>]}>(
{} as SetNonNullableDeep<{readonly a?: [string, number | null, boolean | undefined, ...Array<string | null>] | undefined}, 'a' | 'a.1' | 'a.2'>,
);

// Readonly arrays
expectType<{a?: {b?: readonly [(string | number)?]} | null}>({} as SetNonNullableDeep<{a?: {b?: readonly [(string | number | null)?]} | null}, 'a.b.0'>);
expectType<{a: readonly [string, number, boolean, ...Array<string | undefined>] | undefined}>(
{} as SetNonNullableDeep<{a: readonly [string | null, number | undefined, boolean | null, ...Array<string | undefined>] | undefined}, 'a.0' | 'a.1' | 'a.2'>,
);

// Ignores `Keys` that are already non-nullable
expectType<{a: [string, (number | null)?, boolean?]}>({} as SetNonNullableDeep<{a: [string, (number | null)?, boolean?]}, 'a.0'>);
expectType<{a: [string, number?, boolean?]}>({} as SetNonNullableDeep<{a: [string, (number | null)?, boolean?]}, 'a.0' | 'a.1'>);

// Ignores `Keys` that are not known
// This case is only possible when the array contains a rest element,
// because otherwise the constaint on `KeyPaths` would disallow out of bound keys.
expectType<{a?: readonly [string | null, (number | undefined)?, (boolean | null)?, ...Array<number | null | undefined>] | undefined}>(
{} as SetNonNullableDeep<{a?: readonly [string | null, (number | undefined)?, (boolean | null)?, ...Array<number | null | undefined>] | undefined}, 'a.10'>,
);

// Unions of arrays
expectType<{a: [string] | [string, number?, (boolean | null)?, ...Array<number | null>] | readonly [string, number, (boolean | undefined)?]}>(
{} as SetNonNullableDeep<{a: [string | undefined] | [string | null, number?, (boolean | null)?, ...Array<number | null>] | readonly [string | null | undefined, number | null, (boolean | undefined)?]}, 'a.0' | 'a.1'>,
);

// Labelled tuples
expectType<{a?: [b: string, c: number] | undefined}>({} as SetNonNullableDeep<{a?: [b: string | null, c: number | undefined] | undefined}, 'a.0' | 'a.1'>);

// Non-tuple arrays
expectType<{a: string[]}>({} as SetNonNullableDeep<{a: Array<string | null>}, `a.${number}`>);
expectType<{readonly a: ReadonlyArray<string | number>}>({} as SetNonNullableDeep<{readonly a: ReadonlyArray<string | number | null> | undefined}, 'a' | `a.${number}`>);

// Nested arrays
expectType<{a?: [([string?, (number | null)?] | null)?]}>({} as SetNonNullableDeep<{a?: [([(string | undefined)?, (number | null)?] | null)?] | undefined}, 'a' | 'a.0.0'>);
expectType<{a?: [[(string | undefined)?, number?]?] | undefined}>({} as SetNonNullableDeep<{a?: [([(string | undefined)?, (number | null)?] | null)?] | undefined}, 'a.0.1' | 'a.0'>);
expectType<{a?: [[string | null, number]?] | null}>({} as SetNonNullableDeep<{a?: [([string | null, number | undefined] | null)?] | null}, 'a.0' | 'a.0.1'>);
expectType<{a?: Array<[string | null, number?]> | null}>({} as SetNonNullableDeep<{a?: Array<[string | null, (number | undefined)?]> | null}, `a.${number}.1`>);

// Removes `null` & `undefined` from keys inside arrays
expectType<{a?: Array<{b: number}> | undefined}>({} as SetNonNullableDeep<{a?: Array<{b: number | null}> | undefined}, `a.${number}.b`>);
expectType<{readonly a?: [{readonly b: number}]}>({} as SetNonNullableDeep<{readonly a?: [{readonly b: number | undefined}] | null}, 'a' | 'a.0' | 'a.0.b'>);
expectType<{readonly a: [{readonly b: number}, {c?: string | null}?]}>(
{} as SetNonNullableDeep<{readonly a: [{readonly b: number | null | undefined}, ({c?: string | null} | undefined)?]}, 'a.0.b' | 'a.1' >,
);
expectType<{a?: Array<{b: number; c?: string | null}> | null}>({} as SetNonNullableDeep<{a?: Array<{b: number | undefined; c?: string | null}> | null}, `a.${number}.b`>);
expectType<{a: [{b?: number | null; readonly c: string}]}>({} as SetNonNullableDeep<{a: [{b?: number | null; readonly c: string | undefined}]}, 'a.0.c'>);