Skip to content

Commit

Permalink
Add PartialOnUndefinedDeep type (#426)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
clemclx and sindresorhus committed Jul 30, 2022
1 parent 0869147 commit 1cbd351
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 0 deletions.
1 change: 1 addition & 0 deletions index.d.ts
Expand Up @@ -15,6 +15,7 @@ export {RequireExactlyOne} from './source/require-exactly-one';
export {RequireAllOrNone} from './source/require-all-or-none';
export {RemoveIndexSignature} from './source/remove-index-signature';
export {PartialDeep, PartialDeepOptions} from './source/partial-deep';
export {PartialOnUndefinedDeep, PartialOnUndefinedDeepOptions} from './source/partial-on-undefined-deep';
export {ReadonlyDeep} from './source/readonly-deep';
export {LiteralUnion} from './source/literal-union';
export {Promisable} from './source/promisable';
Expand Down
1 change: 1 addition & 0 deletions readme.md
Expand Up @@ -134,6 +134,7 @@ Click the type names for complete docs.
- [`RequireAllOrNone`](source/require-all-or-none.d.ts) - Create a type that requires all of the given keys or none of the given keys.
- [`RemoveIndexSignature`](source/remove-index-signature.d.ts) - Create a type that only has explicitly defined properties, absent of any index signatures.
- [`PartialDeep`](source/partial-deep.d.ts) - Create a deeply optional version of another type. Use [`Partial<T>`](https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype) if you only need one level deep.
- [`PartialOnUndefinedDeep`](source/partial-on-undefined-deep.d.ts) - Create a deep version of another type where all keys accepting `undefined` type are set to optional.
- [`ReadonlyDeep`](source/readonly-deep.d.ts) - Create a deeply immutable version of an `object`/`Map`/`Set`/`Array` type. Use [`Readonly<T>`](https://www.typescriptlang.org/docs/handbook/utility-types.html#readonlytype) if you only need one level deep.
- [`LiteralUnion`](source/literal-union.d.ts) - Create a union type by combining primitive types and literal types without sacrificing auto-completion in IDEs for the literal type part of the union. Workaround for [Microsoft/TypeScript#29729](https://github.com/Microsoft/TypeScript/issues/29729).
- [`Opaque`](source/opaque.d.ts) - Create an [opaque type](https://codemix.com/opaque-types-in-javascript/).
Expand Down
70 changes: 70 additions & 0 deletions source/partial-on-undefined-deep.d.ts
@@ -0,0 +1,70 @@
import type {BuiltIns} from './internal';
import type {Merge} from './merge';

/**
@see PartialOnUndefinedDeep
*/
export interface PartialOnUndefinedDeepOptions {
/**
Whether to affect the individual elements of arrays and tuples.
@default false
*/
readonly recurseIntoArrays?: boolean;
}

/**
Create a deep version of another type where all keys accepting `undefined` type are set to optional.
This utility type is recursive, transforming at any level deep. By default, it does not affect arrays and tuples items unless you explicitly pass `{recurseIntoArrays: true}` as the second type argument.
Use-cases:
- Make all properties of a type that can be undefined optional to not have to specify keys with undefined value.
@example
```
import type {PartialOnUndefinedDeep} from 'type-fest';
interface Settings {
optionA: string;
optionB: number | undefined;
subOption: {
subOptionA: boolean;
subOptionB: boolean | undefined;
}
};
const testSettings: PartialOnUndefinedDeep<Settings> = {
optionA: 'foo',
// 👉 optionB is now optional and can be omitted
subOption: {
subOptionA: true,
// 👉 subOptionB is now optional as well and can be omitted
},
};
```
@category Object
*/
export type PartialOnUndefinedDeep<T, Options extends PartialOnUndefinedDeepOptions = {}> = T extends Record<any, any> | undefined
? {[KeyType in keyof T as undefined extends T[KeyType] ? KeyType : never]?: PartialOnUndefinedDeepValue<T[KeyType], Options>} extends infer U // Make a partial type with all value types accepting undefined (and set them optional)
? Merge<{[KeyType in keyof T as KeyType extends keyof U ? never : KeyType]: PartialOnUndefinedDeepValue<T[KeyType], Options>}, U> // Join all remaining keys not treated in U
: never // Should not happen
: T;

/**
Utility type to get the value type by key and recursively call `PartialOnUndefinedDeep` to transform sub-objects.
*/
type PartialOnUndefinedDeepValue<T, Options extends PartialOnUndefinedDeepOptions> = T extends BuiltIns | ((...arguments: any[]) => unknown)
? T
: T extends ReadonlyArray<infer U> // Test if type is array or tuple
? Options['recurseIntoArrays'] extends true // Check if option is activated
? U[] extends T // Check if array not tuple
? readonly U[] extends T
? ReadonlyArray<PartialOnUndefinedDeep<U, Options>> // Readonly array treatment
: Array<PartialOnUndefinedDeep<U, Options>> // Mutable array treatment
: PartialOnUndefinedDeep<{[Key in keyof T]: PartialOnUndefinedDeep<T[Key], Options>}, Options> // Tuple treatment
: T
: T extends Record<any, any> | undefined
? PartialOnUndefinedDeep<T, Options>
: unknown;
78 changes: 78 additions & 0 deletions test-d/partial-on-undefined-deep.ts
@@ -0,0 +1,78 @@
import {expectAssignable} from 'tsd';
import type {PartialOnUndefinedDeep} from '../index';

type TestingType = {
function: (() => void) | undefined;
object: {objectKey: 1} | undefined;
objectDeep: {
subObject: string | undefined;
};
string: string | undefined;
union: 'test1' | 'test2' | undefined;
number: number | undefined;
boolean: boolean | undefined;
date: Date | undefined;
regexp: RegExp | undefined;
symbol: symbol | undefined;
null: null | undefined;
record: Record<string, any> | undefined;
map: Map<string, string> | undefined;
set: Set<string> | undefined;
array1: any[] | undefined;
array2: Array<{propertyA: string; propertyB: number | undefined}> | undefined;
readonly1: readonly any[] | undefined;
readonly2: ReadonlyArray<{propertyA: string; propertyB: number | undefined}> | undefined;
tuple: ['test1', {propertyA: string; propertyB: number | undefined}] | undefined;
};

// Default behavior, without recursion into arrays/tuples
declare const foo: PartialOnUndefinedDeep<TestingType>;
expectAssignable<{
function?: TestingType['function'];
object?: TestingType['object'];
objectDeep: {
subObject?: TestingType['objectDeep']['subObject'];
};
string?: TestingType['string'];
union?: TestingType['union'];
number?: TestingType['number'];
boolean?: TestingType['boolean'];
date?: TestingType['date'];
regexp?: TestingType['regexp'];
symbol?: TestingType['symbol'];
null?: TestingType['null'];
record?: TestingType['record'];
map?: TestingType['map'];
set?: TestingType['set'];
array1?: TestingType['array1'];
array2?: TestingType['array2'];
readonly1?: TestingType['readonly1'];
readonly2?: TestingType['readonly2'];
tuple?: TestingType['tuple'];
}>(foo);

// With recursion into arrays/tuples activated
declare const bar: PartialOnUndefinedDeep<TestingType, {recurseIntoArrays: true}>;
expectAssignable<{
function?: TestingType['function'];
object?: TestingType['object'];
objectDeep: {
subObject?: TestingType['objectDeep']['subObject'];
};
string?: TestingType['string'];
union?: TestingType['union'];
number?: TestingType['number'];
boolean?: TestingType['boolean'];
date?: TestingType['date'];
regexp?: TestingType['regexp'];
symbol?: TestingType['symbol'];
null?: TestingType['null'];
record?: TestingType['record'];
map?: TestingType['map'];
set?: TestingType['set'];
array1?: TestingType['array1'];
array2?: Array<{propertyA: string; propertyB?: number | undefined}> | undefined;
readonly1?: TestingType['readonly1'];
readonly2?: ReadonlyArray<{propertyA: string; propertyB?: number | undefined}> | undefined;
tuple?: ['test1', {propertyA: string; propertyB?: number | undefined}] | undefined;
}>(bar);

0 comments on commit 1cbd351

Please sign in to comment.