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 recurseIntoArrays option for PartialDeep #400

Merged
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion index.d.ts
Expand Up @@ -14,7 +14,7 @@ export {RequireAtLeastOne} from './source/require-at-least-one';
export {RequireExactlyOne} from './source/require-exactly-one';
export {RequireAllOrNone} from './source/require-all-or-none';
export {RemoveIndexSignature} from './source/remove-index-signature';
export {PartialDeep} from './source/partial-deep';
export {PartialDeep, PartialDeepOptions} from './source/partial-deep';
export {ReadonlyDeep} from './source/readonly-deep';
export {LiteralUnion} from './source/literal-union';
export {Promisable} from './source/promisable';
Expand Down
64 changes: 48 additions & 16 deletions source/partial-deep.d.ts
@@ -1,5 +1,17 @@
import type {BuiltIns} from './internal';

/**
@see PartialDeep
*/
export interface PartialDeepOptions {
/**
Whether to affect the individual elements of arrays and tuples.

@default true
*/
readonly recurseIntoArrays?: boolean;
}

/**
Create a type from another type with all keys and nested keys set to optional.

Expand Down Expand Up @@ -28,54 +40,74 @@ const applySavedSettings = (savedSettings: PartialDeep<Settings>) => {
settings = applySavedSettings({textEditor: {fontWeight: 500}});
```

By default, this also affects array and tuple types:

```
import type {PartialDeep} from 'type-fest';

interface Settings {
languages: string[];
}

const partialSettings: PartialDeep<Settings> = {
languages: [undefined]
};
```

If this is undesirable, you can pass `{recurseIntoArrays: false}` as the second type argument.

@category Object
@category Array
@category Set
@category Map
*/
export type PartialDeep<T> = T extends BuiltIns
export type PartialDeep<T, Options extends PartialDeepOptions = {}> = T extends BuiltIns
? T
: T extends Map<infer KeyType, infer ValueType>
? PartialMapDeep<KeyType, ValueType>
? PartialMapDeep<KeyType, ValueType, Options>
: T extends Set<infer ItemType>
? PartialSetDeep<ItemType>
? PartialSetDeep<ItemType, Options>
: T extends ReadonlyMap<infer KeyType, infer ValueType>
? PartialReadonlyMapDeep<KeyType, ValueType>
? PartialReadonlyMapDeep<KeyType, ValueType, Options>
: T extends ReadonlySet<infer ItemType>
? PartialReadonlySetDeep<ItemType>
? PartialReadonlySetDeep<ItemType, Options>
: T extends ((...arguments: any[]) => unknown)
? T | undefined
: T extends object
? T extends Array<infer ItemType> // Test for arrays/tuples, per https://github.com/microsoft/TypeScript/issues/35156
? ItemType[] extends T // Test for arrays (non-tuples) specifically
? Array<PartialDeep<ItemType | undefined>> // Recreate relevant array type to prevent eager evaluation of circular reference
: PartialObjectDeep<T> // Tuples behave properly
: PartialObjectDeep<T>
? T extends ReadonlyArray<infer ItemType> // Test for arrays/tuples, per https://github.com/microsoft/TypeScript/issues/35156
? Options['recurseIntoArrays'] extends false // If they opt out of array testing, just use the original type
? T
: ItemType[] extends T // Test for arrays (non-tuples) specifically
? readonly ItemType[] extends T // Differentiate readonly and mutable arrays
? ReadonlyArray<PartialDeep<ItemType | undefined, Options>>
: Array<PartialDeep<ItemType | undefined, Options>>
: PartialObjectDeep<T, Options> // Tuples behave properly
: PartialObjectDeep<T, Options>
: unknown;

/**
Same as `PartialDeep`, but accepts only `Map`s and as inputs. Internal helper for `PartialDeep`.
*/
interface PartialMapDeep<KeyType, ValueType> extends Map<PartialDeep<KeyType>, PartialDeep<ValueType>> {}
interface PartialMapDeep<KeyType, ValueType, Options extends PartialDeepOptions> extends Map<PartialDeep<KeyType, Options>, PartialDeep<ValueType, Options>> {}

/**
Same as `PartialDeep`, but accepts only `Set`s as inputs. Internal helper for `PartialDeep`.
*/
interface PartialSetDeep<T> extends Set<PartialDeep<T>> {}
interface PartialSetDeep<T, Options extends PartialDeepOptions> extends Set<PartialDeep<T, Options>> {}

/**
Same as `PartialDeep`, but accepts only `ReadonlyMap`s as inputs. Internal helper for `PartialDeep`.
*/
interface PartialReadonlyMapDeep<KeyType, ValueType> extends ReadonlyMap<PartialDeep<KeyType>, PartialDeep<ValueType>> {}
interface PartialReadonlyMapDeep<KeyType, ValueType, Options extends PartialDeepOptions> extends ReadonlyMap<PartialDeep<KeyType, Options>, PartialDeep<ValueType, Options>> {}

/**
Same as `PartialDeep`, but accepts only `ReadonlySet`s as inputs. Internal helper for `PartialDeep`.
*/
interface PartialReadonlySetDeep<T> extends ReadonlySet<PartialDeep<T>> {}
interface PartialReadonlySetDeep<T, Options extends PartialDeepOptions> extends ReadonlySet<PartialDeep<T, Options>> {}

/**
Same as `PartialDeep`, but accepts only `object`s as inputs. Internal helper for `PartialDeep`.
*/
type PartialObjectDeep<ObjectType extends object> = {
[KeyType in keyof ObjectType]?: PartialDeep<ObjectType[KeyType]>
type PartialObjectDeep<ObjectType extends object, Options extends PartialDeepOptions> = {
[KeyType in keyof ObjectType]?: PartialDeep<ObjectType[KeyType], Options>
};
27 changes: 27 additions & 0 deletions test-d/partial-deep.ts
Expand Up @@ -63,3 +63,30 @@ type Recurse =
type RecurseObject = {value: Recurse};
const recurseObject: RecurseObject = {value: null};
expectAssignable<PartialDeep<RecurseObject>>(recurseObject);

// Check that recurseIntoArrays: true is the default
expectType<PartialDeep<typeof foo, {recurseIntoArrays: true}>>(partialDeepFoo);
// Check that recurseIntoArrays: false behaves as expected
const partialDeepNoRecurseIntoArraysFoo: PartialDeep<typeof foo, {recurseIntoArrays: false}> = foo;
// These are mostly the same checks as before, but the array/tuple types are different.
expectError(expectType<Partial<typeof foo>>(partialDeepNoRecurseIntoArraysFoo));
const partialDeepNoRecurseIntoArraysBar: PartialDeep<typeof foo.bar, {recurseIntoArrays: false}> = foo.bar;
expectType<typeof partialDeepNoRecurseIntoArraysBar | undefined>(partialDeepNoRecurseIntoArraysFoo.bar);
expectType<((_: string) => void) | undefined>(partialDeepNoRecurseIntoArraysBar.function);
expectAssignable<object | undefined>(partialDeepNoRecurseIntoArraysBar.object);
expectType<string | undefined>(partialDeepNoRecurseIntoArraysBar.string);
expectType<number | undefined>(partialDeepNoRecurseIntoArraysBar.number);
expectType<boolean | undefined>(partialDeepNoRecurseIntoArraysBar.boolean);
expectType<Date | undefined>(partialDeepNoRecurseIntoArraysBar.date);
expectType<RegExp | undefined>(partialDeepNoRecurseIntoArraysBar.regexp);
expectType<symbol | undefined>(partialDeepNoRecurseIntoArraysBar.symbol);
expectType<null | undefined>(partialDeepNoRecurseIntoArraysBar.null);
expectType<undefined>(partialDeepNoRecurseIntoArraysBar.undefined);
expectAssignable<Map<string | undefined, string | undefined> | undefined>(partialDeepNoRecurseIntoArraysBar.map);
expectAssignable<Set<string | undefined> | undefined>(partialDeepNoRecurseIntoArraysBar.set);
expectType<string[] | undefined>(partialDeepNoRecurseIntoArraysBar.array);
expectType<['foo'] | undefined>(partialDeepNoRecurseIntoArraysBar.tuple);
expectAssignable<ReadonlyMap<string | undefined, string | undefined> | undefined>(partialDeepNoRecurseIntoArraysBar.readonlyMap);
expectAssignable<ReadonlySet<string | undefined> | undefined>(partialDeepNoRecurseIntoArraysBar.readonlySet);
expectType<readonly string[] | undefined>(partialDeepNoRecurseIntoArraysBar.readonlyArray);
expectType<readonly ['foo'] | undefined>(partialDeepNoRecurseIntoArraysBar.readonlyTuple);