Skip to content

Commit

Permalink
SetRequiredDeep: Fix handling of unions in nested keys (#1037)
Browse files Browse the repository at this point in the history
  • Loading branch information
som-sm authored Jan 15, 2025
1 parent c6d5142 commit bf5ce3c
Show file tree
Hide file tree
Showing 2 changed files with 177 additions and 33 deletions.
44 changes: 33 additions & 11 deletions source/set-required-deep.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import type {IsAny} from './is-any';
import type {NonRecursiveType, StringToNumber} from './internal';
import type {Paths} from './paths';
import type {SetRequired} from './set-required';
import type {SimplifyDeep} from './simplify-deep';
import type {UnionToTuple} from './union-to-tuple';
import type {RequiredDeep} from './required-deep';
import type {UnknownArray} from './unknown-array';

/**
Expand Down Expand Up @@ -28,19 +32,37 @@ type SomeRequiredDeep = SetRequiredDeep<Foo, 'a' | `c.${number}.d`>;
// d: number // Is now required
// }[]
// }
// Set specific indices in an array to be required.
type ArrayExample = SetRequiredDeep<{a: [number?, number?, number?]}, 'a.0' | 'a.1'>;
//=> {a: [number, number, number?]}
```
@category Object
*/
export type SetRequiredDeep<BaseType, KeyPaths extends Paths<BaseType>> =
BaseType extends NonRecursiveType
export type SetRequiredDeep<BaseType, KeyPaths extends Paths<BaseType>> = IsAny<KeyPaths> extends true
? SimplifyDeep<RequiredDeep<BaseType>>
: SetRequiredDeepHelper<BaseType, UnionToTuple<KeyPaths>>;

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

/**
Makes a single path required in `BaseType`.
*/
type SetRequiredDeepSinglePath<BaseType, KeyPath> = BaseType extends NonRecursiveType
? BaseType
: SimplifyDeep<(
BaseType extends UnknownArray
? {}
: {[K in keyof BaseType as K extends (KeyPaths | StringToNumber<KeyPaths & string>) ? K : never]-?: BaseType[K]}
) & {
[K in keyof BaseType]: Extract<KeyPaths, `${K & (string | number)}.${string}`> extends never
? BaseType[K]
: SetRequiredDeep<BaseType[K], KeyPaths extends `${K & (string | number)}.${infer Rest extends Paths<BaseType[K]>}` ? Rest : never>
}>;
: KeyPath extends `${infer Property}.${infer RestPath}`
? {
[Key in keyof BaseType]: Property extends `${Key & (string | number)}`
? SetRequiredDeepSinglePath<BaseType[Key], RestPath>
: BaseType[Key];
}
: SetRequired<BaseType, (KeyPath | StringToNumber<KeyPath & string>) & keyof BaseType>;
166 changes: 144 additions & 22 deletions test-d/set-required-deep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,42 +21,164 @@ expectType<{a: number; b?: {c?: string}}>(variation4);
declare const variation5: SetRequiredDeep<{a?: '1'; b?: {c?: boolean}} | {a?: '2'; b?: {c?: boolean}}, 'a'>;
expectType<{a: '1'; b?: {c?: boolean}} | {a: '2'; b?: {c?: boolean}}>(variation5);

// Set array key to required
// Set key with array type to required
declare const variation6: SetRequiredDeep<{a?: Array<{b?: number}>}, 'a'>;
expectType<{a: Array<{b?: number}>}>(variation6);

// Set key inside array to required
declare const variation7: SetRequiredDeep<{a?: Array<{b?: number}>}, `a.${number}.b`>;
expectType<{a?: Array<{b: number}>}>(variation7);

// Set only specified keys inside array to required
declare const variation8: SetRequiredDeep<{a?: Array<{b?: number; c?: string}>}, `a.${number}.b`>;
expectType<{a?: Array<{b: number; c?: string}>}>(variation8);

// Can set both root and nested keys to required
declare const variation9: SetRequiredDeep<{a?: number; b?: {c?: string}}, 'b' | 'b.c'>;
expectType<{a?: number; b: {c: string}}>(variation9);
declare const variation7: SetRequiredDeep<{a?: number; b?: {c?: string}}, 'b' | 'b.c'>;
expectType<{a?: number; b: {c: string}}>(variation7);

// Preserves required root keys
declare const variation10: SetRequiredDeep<{a: 1; b: {c?: 1}}, 'b.c'>;
expectType<{a: 1; b: {c: 1}}>(variation10);
declare const variation8: SetRequiredDeep<{a: 1; b: {c?: 1}}, 'b.c'>;
expectType<{a: 1; b: {c: 1}}>(variation8);

// Preserves union in root keys
declare const variation11: SetRequiredDeep<{a: 1; b: {c?: 1} | number}, 'b.c'>;
expectType<{a: 1; b: {c: 1} | number}>(variation11);
declare const variation9: SetRequiredDeep<{a: 1; b: {c?: 1} | number}, 'b.c'>;
expectType<{a: 1; b: {c: 1} | number}>(variation9);

// Preserves readonly
declare const variation10: SetRequiredDeep<{a: 1; readonly b: {c?: 1}}, 'b.c'>;
expectType<{a: 1; readonly b: {c: 1}}>(variation10);

// Preserves readonly in root keys
declare const variation12: SetRequiredDeep<{a: 1; readonly b: {c?: 1}}, 'b.c'>;
expectType<{a: 1; readonly b: {c: 1}}>(variation12);
declare const variation11: SetRequiredDeep<{readonly a?: 1; readonly b?: {readonly c?: 1}}, 'a' | 'b'>;
expectType<{readonly a: 1; readonly b: {readonly c?: 1}}>(variation11);

declare const variation12: SetRequiredDeep<{readonly a?: 1; readonly b?: {readonly c?: 1}}, 'a' | 'b' | 'b.c'>;
expectType<{readonly a: 1; readonly b: {readonly c: 1}}>(variation12);

// Works with number keys
declare const variation13: SetRequiredDeep<{0: 1; 1: {2?: string}}, '1.2'>;
expectType<{0: 1; 1: {2: string}}>(variation13);

declare const variation14: SetRequiredDeep<{0?: 1; 1?: {2?: string}}, 0 | 1>;
expectType<{0: 1; 1: {2?: string}}>(variation14);

// Multiple keys
declare const variation14: SetRequiredDeep<{a?: 1; b?: {c?: 2}; d?: {e?: {f?: 2}; g?: 3}}, 'a' | 'b' | 'b.c' | 'd.e.f' | 'd.g'>;
expectType<{a: 1; b: {c: 2}; d?: {e?: {f: 2}; g: 3}}>(variation14);
declare const variation15: SetRequiredDeep<{a?: 1; b?: {c?: 2}; d?: {e?: {f?: 2}; g?: 3}}, 'a' | 'b' | 'b.c' | 'd.e.f' | 'd.g'>;
expectType<{a: 1; b: {c: 2}; d?: {e?: {f: 2}; g: 3}}>(variation15);

// Index signatures
declare const variation15: SetRequiredDeep<{[x: string]: any; a?: number; b?: {c?: number}}, 'a' | 'b.c'>;
expectType<{[x: string]: any; a: number; b?: {c: number}}>(variation15);
declare const variation16: SetRequiredDeep<{[x: string]: any; a?: number; b?: {c?: number}}, 'a' | 'b.c'>;
expectType<{[x: string]: any; a: number; b?: {c: number}}>(variation16);

// Preserves union in nested keys
declare const variation17: SetRequiredDeep<{a: 1; b?: {c?: 1} | number}, 'b'>;
expectType<{a: 1; b: {c?: 1} | number}>(variation17);

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

// Works with number keys containing dots
// NOTE: Passing "1.2" instead of 1.2 will treat it as a path instead of a key
declare const variation19: SetRequiredDeep<{1.2?: string; 1?: {2?: string}}, 1.2>;
expectType<{1.2: string; 1?: {2?: string}}>(variation19);

declare const variation20: SetRequiredDeep<{1.2?: string; 1?: {2?: string}}, '1.2'>;
expectType<{1.2?: string; 1?: {2: string}}>(variation20);

declare const variation21: SetRequiredDeep<{1.2?: string; 1?: {2?: string}}, 1.2 | '1.2'>;
expectType<{1.2: string; 1?: {2: string}}>(variation21);

// Works with unions
declare const variation22: SetRequiredDeep<{a?: {readonly b?: number}} | {readonly b?: {c?: number[]}}, 'a.b' | 'b' | 'b.c'>;
expectType<{a?: {readonly b: number}} | {readonly b: {c: number[]}}>(variation22);

// Works with `KeyPaths` containing template literals
declare const variation23: SetRequiredDeep<{a?: number; b?: {c?: number} | {d?: number}}, `b.${'c' | 'd'}`>;
expectType<{a?: number; b?: {c: number} | {d: number}}>(variation23);

declare const variation24: SetRequiredDeep<
{a?: number; b?: {readonly c?: {1?: number}} | {d?: {1?: number}}}, 'a' | `b.${'c' | 'd'}.1`
>;
expectType<{a: number; b?: {readonly c?: {1: number}} | {d?: {1: number}}}>(variation24);

// Calls `RequiredDeep` when `KeyPaths` is `any`
declare const variation25: SetRequiredDeep<{a?: number; readonly b?: {c?: string}}, any>;
expectType<{a: number; readonly b: {c: string}}>(variation25);

// Does nothing when `KeyPaths` is `never`
declare const variation26: SetRequiredDeep<{a?: number; readonly b?: {c?: string}}, never>;
expectType<{a?: number; readonly b?: {c?: string}}>(variation26);

// =================
// Works with arrays
// =================

// All optional elements
expectType<{a?: [string, number, boolean?]}>({} as SetRequiredDeep<{a?: [string?, number?, boolean?]}, 'a.0' | 'a.1'>);

// Mix of optional and required elements
expectType<{a: readonly [string, number, boolean]}>({} as SetRequiredDeep<{a: readonly [string, number?, boolean?]}, 'a.1' | 'a.2'>);

// Mix of optional and rest elements
expectType<{readonly a: [string, number, boolean?, ...number[]]}>({} as SetRequiredDeep<{readonly a: [string?, number?, boolean?, ...number[]]}, 'a.0' | 'a.1'>);

// Mix of optional, required, and rest elements
expectType<{readonly a?: [string, number, boolean, ...string[]]}>({} as SetRequiredDeep<{readonly a?: [string, number?, boolean?, ...string[]]}, 'a.1' | 'a.2'>);

// Works with readonly arrays
expectType<{a?: {b?: readonly [(string | number)]}}>({} as SetRequiredDeep<{a?: {b?: readonly [(string | number)?]}}, 'a.b.0'>);
expectType<{a: readonly [string, number, boolean, ...string[]]}>(
{} as SetRequiredDeep<{a?: readonly [string, number?, boolean?, ...string[]]}, 'a' | 'a.1' | 'a.2'>,
);

// Ignores `Keys` that are already required
expectType<{a: [string, number?, boolean?]}>({} as SetRequiredDeep<{a: [string, number?, boolean?]}, 'a.0'>);

// 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?, number?, boolean?, ...number[]]}>(
{} as SetRequiredDeep<{a?: readonly [string?, number?, boolean?, ...number[]]}, 'a.10'>,
);

// Marks all keys as required, if `Keys` is `number`.
// This case is only possible when the array contains a rest element,
// because otherwise the constaint on `KeyPaths` would be stricter.
expectType<{a?: readonly [string, number, boolean, ...number[]]}>(
{} as SetRequiredDeep<{a?: readonly [string?, number?, boolean?, ...number[]]}, `a.${number}`>,
);

// Preserves `| undefined`, similar to how built-in `Required` works.
expectType<{a: [string | undefined, number | undefined, boolean]}>({} as SetRequiredDeep<{a: [string | undefined, (number | undefined)?, boolean?]}, 'a.0' | 'a.1' | 'a.2'>);
expectType<{a: readonly [string | undefined, (number | undefined)?, boolean?]}>(
{} as SetRequiredDeep<{a: readonly [(string | undefined)?, (number | undefined)?, boolean?]}, 'a.0'>,
);

// Optional elements cannot appear after required ones, `Keys` leading to such situations are ignored.
expectType<{a: [string?, number?, boolean?]}>({} as SetRequiredDeep<{a: [string?, number?, boolean?]}, 'a.1' | 'a.2'>); // `a.1` and `a.2` can't be required when `a.0` is optional
expectType<{a: [string, number, boolean?, string?, string?]}>(
{} as SetRequiredDeep<{a: [string?, number?, boolean?, string?, string?]}, 'a.0' | 'a.1' | 'a.3'>, // `a.3` can't be required when `a.2` is optional
);
expectType<{a: readonly [string | undefined, number?, boolean?, ...string[]]}>(
{} as SetRequiredDeep<{a?: readonly [string | undefined, number?, boolean?, ...string[]]}, 'a' | 'a.2'>, // `a.2` can't be required when `a.1` is optional
);

// Works with unions of arrays
expectType<{a: [string] | [string, number, boolean?, ...number[]] | readonly [string, number, boolean?]}>(
{} as SetRequiredDeep<{a: [string?] | [string, number?, boolean?, ...number[]] | readonly [string, number?, boolean?]}, 'a.0' | 'a.1'>,
);

// Works with labelled tuples
expectType<{a?: [b: string, c: number]}>({} as SetRequiredDeep<{a?: [b?: string, c?: number]}, 'a.0' | 'a.1'>);

// Non tuple arrays are left unchanged
expectType<{a: string[]}>({} as SetRequiredDeep<{a: string[]}, `a.${number}`>);
expectType<{readonly a: ReadonlyArray<string | number>}>({} as SetRequiredDeep<{readonly a?: ReadonlyArray<string | number>}, 'a' | `a.${number}`>);

// Works with nested arrays
expectType<{a?: [[string, number?]?]}>({} as SetRequiredDeep<{a?: [[string?, number?]?]}, 'a.0.0'>);
expectType<{a?: [[string, number]]}>({} as SetRequiredDeep<{a?: [[string?, number?]?]}, 'a.0' | 'a.0.0' | 'a.0.1'>);
expectType<{a?: Array<[string, number?]>}>({} as SetRequiredDeep<{a?: Array<[string?, number?]>}, `a.${number}.0`>);

// Set key inside array to required
expectType<{a?: Array<{b: number}>}>({} as SetRequiredDeep<{a?: Array<{b?: number}>}, `a.${number}.b`>);
expectType<{readonly a?: [{readonly b: number}]}>({} as SetRequiredDeep<{readonly a?: [{readonly b?: number}]}, 'a.0.b'>);
expectType<{readonly a: [{readonly b: number}, {c?: string}]}>(
{} as SetRequiredDeep<{readonly a?: [{readonly b?: number}, {c?: string}?]}, 'a' | 'a.0.b' | 'a.1' >,
);

// Set only specified keys inside array to required
expectType<{a?: Array<{b: number; c?: string}>}>({} as SetRequiredDeep<{a?: Array<{b?: number; c?: string}>}, `a.${number}.b`>);
expectType<{a: [{b?: number; readonly c: string}]}>({} as SetRequiredDeep<{a: [{b?: number; readonly c?: string}]}, 'a.0.c'>);

0 comments on commit bf5ce3c

Please sign in to comment.