-
-
Notifications
You must be signed in to change notification settings - Fork 546
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
UndefinedOnPartialDeep
type (#700)
Co-authored-by: Sindre Sorhus <[email protected]>
- Loading branch information
1 parent
0517399
commit d8b44cb
Showing
6 changed files
with
172 additions
and
1 deletion.
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
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,81 @@ | ||
import type {BuiltIns} from './internal'; | ||
import type {Merge} from './merge'; | ||
|
||
/** | ||
Create a deep version of another type where all optional keys are set to also accept `undefined`. | ||
Note: This is only needed when the [`exactOptionalPropertyTypes`](https://www.typescriptlang.org/tsconfig#exactOptionalPropertyTypes) TSConfig setting is enabled. | ||
Use-cases: | ||
- When `exactOptionalPropertyTypes` is enabled, an object like `{a: undefined}` is not assignable to the type `{a?: number}`. You can use `UndefinedOnPartialDeep<{a?: number}>` to make it assignable. | ||
@example | ||
``` | ||
import type {UndefinedOnPartialDeep} from 'type-fest'; | ||
interface Settings { | ||
optionA: string; | ||
optionB?: number; | ||
subOption: { | ||
subOptionA: boolean; | ||
subOptionB?: boolean; | ||
} | ||
}; | ||
const testSettingsA: Settings = { | ||
optionA: 'foo', | ||
optionB: undefined, // TypeScript error if `exactOptionalPropertyTypes` is true. | ||
subOption: { | ||
subOptionA: true, | ||
subOptionB: undefined, // TypeScript error if `exactOptionalPropertyTypes` is true | ||
}, | ||
}; | ||
const testSettingsB: UndefinedOnPartialDeep<Settings> = { | ||
optionA: 'foo', | ||
optionB: undefined, // 👉 `optionB` can be set to undefined now. | ||
subOption: { | ||
subOptionA: true, | ||
subOptionB: undefined, // 👉 `subOptionB` can be set to undefined now. | ||
}, | ||
}; | ||
``` | ||
*/ | ||
export type UndefinedOnPartialDeep<T> = | ||
// Handle built-in type and function | ||
T extends BuiltIns | Function | ||
? T | ||
// Handle tuple and array | ||
: T extends readonly unknown[] | ||
? UndefinedOnPartialList<T> | ||
// Handle map and readonly map | ||
: T extends Map<infer K, infer V> | ||
? Map<K, UndefinedOnPartialDeep<V>> | ||
: T extends ReadonlyMap<infer K, infer V> | ||
? ReadonlyMap<K, UndefinedOnPartialDeep<V>> | ||
// Handle set and readonly set | ||
: T extends Set<infer K> | ||
? Set<UndefinedOnPartialDeep<K>> | ||
: T extends ReadonlySet<infer K> | ||
? ReadonlySet<UndefinedOnPartialDeep<K>> | ||
// Handle object | ||
: T extends Record<any, any> | ||
? { | ||
[KeyType in keyof T]: undefined extends T[KeyType] | ||
? UndefinedOnPartialDeep<T[KeyType]> | undefined | ||
: UndefinedOnPartialDeep<T[KeyType]> | ||
} | ||
: T; // If T is not builtins / function / array / map / set / object, return T | ||
|
||
// Handle tuples and arrays | ||
type UndefinedOnPartialList<T extends readonly unknown[]> = T extends [] | ||
? [] | ||
: T extends [infer F, ...infer R] | ||
? [UndefinedOnPartialDeep<F>, ...UndefinedOnPartialDeep<R>] | ||
: T extends readonly [infer F, ...infer R] | ||
? readonly [UndefinedOnPartialDeep<F>, ...UndefinedOnPartialDeep<R>] | ||
: T extends Array<infer F> | ||
? Array<UndefinedOnPartialDeep<F>> | ||
: T extends ReadonlyArray<infer F> | ||
? ReadonlyArray<UndefinedOnPartialDeep<F>> | ||
: never; |
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,7 @@ | ||
{ | ||
"extends": "../../tsconfig", | ||
"compilerOptions": { | ||
"exactOptionalPropertyTypes": true, | ||
}, | ||
"exclude": [] | ||
} |
80 changes: 80 additions & 0 deletions
80
test-d/undefined-on-partial-deep/undefined-on-partial-deep.ts
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,80 @@ | ||
/** | ||
@note This file is used for testing by `tsc` but not `tsd`, so we can just test assignable. | ||
*/ | ||
import {expectAssignable} from 'tsd'; | ||
import type {UndefinedOnPartialDeep} from '../../source/undefined-on-partial-deep'; | ||
|
||
type TestType1 = UndefinedOnPartialDeep<{required: string; optional?: string; optional2?: number; optional3?: string}>; | ||
expectAssignable<TestType1>({required: '', optional: undefined, optional2: 1}); | ||
|
||
type TestType2 = UndefinedOnPartialDeep<{optional?: string | undefined}>; | ||
expectAssignable<TestType2>({optional: undefined}); | ||
|
||
type TestType3 = UndefinedOnPartialDeep<{requiredWithUndefind: string | undefined}>; | ||
expectAssignable<TestType3>({requiredWithUndefind: undefined}); | ||
|
||
// Test null and undefined | ||
type NullType = UndefinedOnPartialDeep<{null?: null}>; | ||
expectAssignable<NullType>({null: undefined}); | ||
type UndefinedType = UndefinedOnPartialDeep<{ud?: undefined}>; | ||
expectAssignable<UndefinedType>({ud: undefined}); | ||
|
||
// Test mixed types | ||
type MixedType = UndefinedOnPartialDeep<{ | ||
required: string; | ||
union?: 'test1' | 'test2'; | ||
boolean?: boolean; | ||
string?: string; | ||
symbol?: symbol; | ||
date?: Date; | ||
regExp?: RegExp; | ||
func?: (args0: string, args1: number) => boolean; | ||
}>; | ||
expectAssignable<MixedType>({ | ||
required: '', | ||
union: undefined, | ||
boolean: undefined, | ||
string: undefined, | ||
symbol: undefined, | ||
date: undefined, | ||
regExp: undefined, | ||
func: undefined, | ||
}); | ||
|
||
// Test object | ||
type ObjectType = UndefinedOnPartialDeep<{obj?: {key: string}}>; | ||
expectAssignable<ObjectType>({obj: undefined}); | ||
|
||
type ObjectDeepType = UndefinedOnPartialDeep<{obj?: {subObj?: {key?: string}}}>; | ||
expectAssignable<ObjectDeepType>({obj: undefined}); | ||
expectAssignable<ObjectDeepType>({obj: {subObj: undefined}}); | ||
expectAssignable<ObjectDeepType>({obj: {subObj: {key: undefined}}}); | ||
|
||
// Test map | ||
type MapType = UndefinedOnPartialDeep<{map?: Map<string, {key?: string}>}>; | ||
expectAssignable<MapType>({map: undefined}); | ||
expectAssignable<MapType>({map: new Map([['', {key: undefined}]])}); | ||
|
||
// Test set | ||
type SetType = UndefinedOnPartialDeep<{set?: Set<{key?: string}>}>; | ||
expectAssignable<SetType>({set: undefined}); | ||
expectAssignable<SetType>({set: new Set([{key: undefined}])}); | ||
|
||
// Test array and tuple | ||
type TupleType = UndefinedOnPartialDeep<{tuple?: [string, number]}>; | ||
expectAssignable<TupleType>({tuple: undefined}); | ||
type ArrayType = UndefinedOnPartialDeep<{array?: string[]}>; | ||
expectAssignable<ArrayType>({array: undefined}); | ||
type ArrayDeepType = UndefinedOnPartialDeep<{array?: Array<{subArray?: string[]}>}>; | ||
expectAssignable<ArrayDeepType>({array: undefined}); | ||
expectAssignable<ArrayDeepType>({array: [{subArray: undefined}]}); | ||
type ObjectListType = UndefinedOnPartialDeep<{array?: Array<{key?: string}>}>; | ||
expectAssignable<ObjectListType>({array: undefined}); | ||
expectAssignable<ObjectListType>({array: [{key: undefined}]}); | ||
|
||
// Test readonly array | ||
type ReadonlyType = UndefinedOnPartialDeep<{readonly?: readonly string[]}>; | ||
expectAssignable<ReadonlyType>({readonly: undefined}); | ||
// eslint-disable-next-line @typescript-eslint/array-type | ||
type ReadonlyArrayTest = UndefinedOnPartialDeep<{readonly?: ReadonlyArray<string>}>; | ||
expectAssignable<ReadonlyArrayTest>({readonly: undefined}); |