Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
PartialOnUndefinedDeep
type (#426)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
- Loading branch information
1 parent
0869147
commit 1cbd351
Showing
4 changed files
with
150 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); |