From 1cbd351e8f114a98648e4b933cbb8567be8acafc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Cloux?= Date: Sat, 30 Jul 2022 13:57:04 +0200 Subject: [PATCH] Add `PartialOnUndefinedDeep` type (#426) Co-authored-by: Sindre Sorhus --- index.d.ts | 1 + readme.md | 1 + source/partial-on-undefined-deep.d.ts | 70 ++++++++++++++++++++++++ test-d/partial-on-undefined-deep.ts | 78 +++++++++++++++++++++++++++ 4 files changed, 150 insertions(+) create mode 100644 source/partial-on-undefined-deep.d.ts create mode 100644 test-d/partial-on-undefined-deep.ts diff --git a/index.d.ts b/index.d.ts index 4a93673e5..95eba8c22 100644 --- a/index.d.ts +++ b/index.d.ts @@ -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'; diff --git a/readme.md b/readme.md index df0ed4980..5e8d46c6a 100644 --- a/readme.md +++ b/readme.md @@ -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`](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`](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/). diff --git a/source/partial-on-undefined-deep.d.ts b/source/partial-on-undefined-deep.d.ts new file mode 100644 index 000000000..d2fe3cad3 --- /dev/null +++ b/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 = { + 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 extends Record | undefined + ? {[KeyType in keyof T as undefined extends T[KeyType] ? KeyType : never]?: PartialOnUndefinedDeepValue} 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}, 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 extends BuiltIns | ((...arguments: any[]) => unknown) + ? T + : T extends ReadonlyArray // 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> // Readonly array treatment + : Array> // Mutable array treatment + : PartialOnUndefinedDeep<{[Key in keyof T]: PartialOnUndefinedDeep}, Options> // Tuple treatment + : T + : T extends Record | undefined + ? PartialOnUndefinedDeep + : unknown; diff --git a/test-d/partial-on-undefined-deep.ts b/test-d/partial-on-undefined-deep.ts new file mode 100644 index 000000000..e1e8444df --- /dev/null +++ b/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 | undefined; + map: Map | undefined; + set: Set | 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; +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; +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);