Skip to content

Commit

Permalink
Add UndefinedOnPartialDeep type (#700)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <[email protected]>
  • Loading branch information
Emiyaaaaa and sindresorhus authored Oct 18, 2023
1 parent 0517399 commit d8b44cb
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 1 deletion.
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export type {PickIndexSignature} from './source/pick-index-signature';
export type {PartialDeep, PartialDeepOptions} from './source/partial-deep';
export type {RequiredDeep} from './source/required-deep';
export type {PartialOnUndefinedDeep, PartialOnUndefinedDeepOptions} from './source/partial-on-undefined-deep';
export type {UndefinedOnPartialDeep} from './source/undefined-on-partial-deep';
export type {ReadonlyDeep} from './source/readonly-deep';
export type {LiteralUnion} from './source/literal-union';
export type {Promisable} from './source/promisable';
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"node": ">=16"
},
"scripts": {
"test": "xo && tsd && tsc && node script/test/source-files-extension.js"
"test": "xo && tsd && tsc && npm run test:undefined-on-partial-deep && node script/test/source-files-extension.js",
"test:undefined-on-partial-deep": "cd test-d/undefined-on-partial-deep && tsc --project tsconfig.json"
},
"files": [
"index.d.ts",
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ Click the type names for complete docs.
- [`PickIndexSignature`](source/pick-index-signature.d.ts) - Pick only index signatures from the given object type, leaving out all explicitly defined properties.
- [`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.
- [`UndefinedOnPartialDeep`](source/undefined-on-partial-deep.d.ts) - Create a deep version of another type where all optional keys are set to also accept `undefined`.
- [`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).
- [`Tagged`](source/opaque.d.ts) - Create a [tagged type](https://medium.com/@KevinBGreene/surviving-the-typescript-ecosystem-branding-and-type-tagging-6cf6e516523d) that can support [multiple tags](https://github.com/sindresorhus/type-fest/issues/665) if needed.
Expand Down
81 changes: 81 additions & 0 deletions source/undefined-on-partial-deep.d.ts
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;
7 changes: 7 additions & 0 deletions test-d/undefined-on-partial-deep/tsconfig.json
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 test-d/undefined-on-partial-deep/undefined-on-partial-deep.ts
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});

0 comments on commit d8b44cb

Please sign in to comment.