Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export type {PickDeep} from './source/pick-deep.d.ts';
export type {OmitDeep} from './source/omit-deep.d.ts';
export type {PartialOnUndefinedDeep, PartialOnUndefinedDeepOptions} from './source/partial-on-undefined-deep.d.ts';
export type {UndefinedOnPartialDeep} from './source/undefined-on-partial-deep.d.ts';
export type {UndefinableDeep} from './source/undefinable-deep.d.ts';
export type {ReadonlyDeep} from './source/readonly-deep.d.ts';
export type {LiteralUnion} from './source/literal-union.d.ts';
export type {Promisable} from './source/promisable.d.ts';
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ Click the type names for complete docs.
- [`PartialDeep`](source/partial-deep.d.ts) - Create a deeply optional version of another type.
- [`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`.
- [`UndefinableDeep`](source/undefinable-deep.d.ts) - Create a deep version of another type where leaf values also accept `undefined`, keeping object containers present.
- [`UnwrapPartial`](source/unwrap-partial.d.ts) - Revert the `Partial` modifier on an object type.
- [`UnwrapRequired`](source/unwrap-required.d.ts) - Revert the `Required` modifier on an object type.
- [`ReadonlyDeep`](source/readonly-deep.d.ts) - Create a deeply immutable version of another type.
Expand Down
74 changes: 74 additions & 0 deletions source/undefinable-deep.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type {BuiltIns} from './internal/index.d.ts';
import type {IsNever} from './is-never.d.ts';

/**
Create a deep version of another type where leaf values also accept `undefined`, keeping object containers present.

Unlike {@link PartialDeep}, this type keeps required properties required. It widens primitive values, `Date`, `RegExp`, functions, and constructors to accept `undefined`, while preserving the structure of nested objects, arrays, tuples, maps, sets, and promises. Optional and `readonly` modifiers are preserved.

Use this to model form state that retains its nested structure while individual fields are cleared or have not yet been populated.

@example
```
import type {UndefinableDeep} from 'type-fest';

type Settings = {
textEditor: {
fontSize: number;
fontColor: string;
};
autosave: boolean;
};

type DraftSettings = UndefinableDeep<Settings>;
//=> {
// textEditor: {
// fontSize: number | undefined;
// fontColor: string | undefined;
// };
// autosave: boolean | undefined;
// }

const draft: DraftSettings = {
textEditor: {
fontSize: undefined, // Present but not yet chosen
fontColor: undefined,
},
autosave: undefined,
};
```

The transformation recurses into object properties and collection elements. For example, `string[]` becomes `Array<string | undefined>`, and `Promise<string>` becomes `Promise<string | undefined>`. An object-valued property or collection element stays an object unless its original type already includes `undefined`.

Functions and constructors are leaves: their signatures and attached properties are preserved unchanged, including generic and overloaded signatures. Weak collections exclude `undefined` from transformed keys because it is not a valid `WeakMap` key or `WeakSet` item. A `never` leaf becomes `undefined`.

@see {@link UndefinedOnPartialDeep}

@category Object
@category Array
@category Set
@category Map
*/
export type UndefinableDeep<Type> = IsNever<Type> extends true
? undefined
: Type extends BuiltIns | Function
? Type | undefined
: Type extends Map<infer KeyType, infer ValueType>
? Map<UndefinableDeep<KeyType>, UndefinableDeep<ValueType>>
: Type extends Set<infer ItemType>
? Set<UndefinableDeep<ItemType>>
: Type extends ReadonlyMap<infer KeyType, infer ValueType>
? ReadonlyMap<UndefinableDeep<KeyType>, UndefinableDeep<ValueType>>
: Type extends ReadonlySet<infer ItemType>
? ReadonlySet<UndefinableDeep<ItemType>>
: Type extends WeakMap<infer KeyType, infer ValueType>
? WeakMap<Extract<UndefinableDeep<KeyType>, WeakKey>, UndefinableDeep<ValueType>>
: Type extends WeakSet<infer ItemType>
? WeakSet<Extract<UndefinableDeep<ItemType>, WeakKey>>
: Type extends Promise<infer ValueType>
? Promise<UndefinableDeep<ValueType>>
: Type extends object
? {[KeyType in keyof Type]: UndefinableDeep<Type[KeyType]>}
: Type | undefined;

export {};
126 changes: 126 additions & 0 deletions test-d/undefinable-deep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import {expectAssignable, expectNotAssignable, expectType} from 'tsd';
import type {IsEqual, UndefinableDeep} from '../index.d.ts';

// Leaf values accept undefined, but required object containers stay present.
expectType<UndefinableDeep<{a: string; b: number}>>({} as {a: string | undefined; b: number | undefined});
expectType<UndefinableDeep<{a: {b: {c: string}}}>>({} as {a: {b: {c: string | undefined}}});
expectNotAssignable<UndefinableDeep<{a: {b: string}}>>({a: undefined});
expectNotAssignable<UndefinableDeep<{a: {b: string}}>>({});
expectNotAssignable<UndefinableDeep<{a: {b: string}}>>({a: {}});

// Optionality, explicit undefined unions, and readonly modifiers are preserved.
expectType<UndefinableDeep<{a?: string; readonly b: number}>>({} as {a?: string | undefined; readonly b: number | undefined});
expectType<IsEqual<UndefinableDeep<{readonly a: {b: string}}>, {readonly a: {b: string | undefined}}>>(true);
expectAssignable<UndefinableDeep<{a?: string}>>({a: undefined});
expectType<UndefinableDeep<{a: {b: string} | undefined}>>({} as {a: {b: string | undefined} | undefined});
expectType<UndefinableDeep<{a?: {b: string}}>>({} as {a?: {b: string | undefined} | undefined});
expectNotAssignable<UndefinableDeep<{a: string}>>({});

// Built-ins are leaves, both at the top level and inside objects.
expectType<UndefinableDeep<{a: Date; b: RegExp}>>({} as {a: Date | undefined; b: RegExp | undefined});
expectType<UndefinableDeep<string>>({} as string | undefined);
expectType<UndefinableDeep<number>>({} as number | undefined);
expectType<UndefinableDeep<boolean>>({} as boolean | undefined);
expectType<UndefinableDeep<bigint>>({} as bigint | undefined);
expectType<UndefinableDeep<symbol>>({} as symbol | undefined);
expectType<UndefinableDeep<Date>>({} as Date | undefined);
expectType<UndefinableDeep<RegExp>>({} as RegExp | undefined);
expectType<UndefinableDeep<null>>(undefined as null | undefined);
expectType<UndefinableDeep<undefined>>(undefined);
declare const voidLeaf: void | undefined;
expectType<UndefinableDeep<void>>(voidLeaf);

// Index signatures and unions recurse into object values without discarding containers.
expectType<UndefinableDeep<{[key: string]: {a: number}}>>({} as {[key: string]: {a: number | undefined}});
expectType<UndefinableDeep<{a: number} | {b: string}>>({} as {a: number | undefined} | {b: string | undefined});
expectType<UndefinableDeep<string | {a: number}>>({} as string | {a: number | undefined} | undefined);

// Arrays widen primitive elements; object elements remain present.
expectType<UndefinableDeep<string[]>>({} as Array<string | undefined>);
expectType<UndefinableDeep<readonly string[]>>({} as ReadonlyArray<string | undefined>);
expectType<UndefinableDeep<ReadonlyArray<{a: number}>>>({} as ReadonlyArray<{a: number | undefined}>);
expectType<UndefinableDeep<{a: Array<{b: number}>}>>({} as {a: Array<{b: number | undefined}>});
expectNotAssignable<UndefinableDeep<{a: string[]}>>({a: undefined});
expectNotAssignable<UndefinableDeep<Array<{a: number}>>>([undefined]);

// Homomorphic tuple mapping preserves labels, readonly, optional, and rest elements.
expectType<UndefinableDeep<[]>>({} as []);
expectType<UndefinableDeep<readonly []>>({} as readonly []);
expectType<UndefinableDeep<[name: string, count: number]>>({} as [name: string | undefined, count: number | undefined]);
expectType<UndefinableDeep<[string, number?]>>({} as [string | undefined, (number | undefined)?]);
expectType<UndefinableDeep<[string, ...number[]]>>({} as [string | undefined, ...Array<number | undefined>]);
expectType<UndefinableDeep<[...string[], number]>>({} as [...Array<string | undefined>, number | undefined]);
expectType<UndefinableDeep<readonly [string, {a: number}]>>({} as readonly [string | undefined, {a: number | undefined}]);
expectAssignable<UndefinableDeep<[string, number?]>>([undefined, undefined]);
expectNotAssignable<UndefinableDeep<[string, number]>>([undefined]);
expectNotAssignable<UndefinableDeep<[{a: number}]>>([undefined]);

// Collections widen primitive keys, values, and items consistently.
expectType<UndefinableDeep<Map<string, number>>>({} as Map<string | undefined, number | undefined>);
expectType<UndefinableDeep<ReadonlyMap<string, {a: number}>>>({} as ReadonlyMap<string | undefined, {a: number | undefined}>);
expectType<UndefinableDeep<Map<{a: number}, string>>>({} as Map<{a: number | undefined}, string | undefined>);
expectType<UndefinableDeep<Set<string>>>({} as Set<string | undefined>);
expectType<UndefinableDeep<ReadonlySet<string>>>({} as ReadonlySet<string | undefined>);
expectType<UndefinableDeep<Set<{a: number}>>>({} as Set<{a: number | undefined}>);
expectType<UndefinableDeep<ReadonlySet<{a: number}>>>({} as ReadonlySet<{a: number | undefined}>);
expectNotAssignable<UndefinableDeep<{a: Map<string, number>}>>({a: undefined});
expectNotAssignable<UndefinableDeep<{a: Set<string>}>>({a: undefined});

// Weak collections cannot acquire undefined keys, even when their keys are leaves.
expectType<UndefinableDeep<WeakMap<{k: string}, number>>>({} as WeakMap<{k: string | undefined}, number | undefined>);
expectType<UndefinableDeep<WeakMap<Date, RegExp>>>({} as WeakMap<Date, RegExp | undefined>);
expectType<UndefinableDeep<WeakMap<symbol, string>>>({} as WeakMap<symbol, string | undefined>);
expectType<UndefinableDeep<WeakSet<{a: number}>>>({} as WeakSet<{a: number | undefined}>);
expectType<UndefinableDeep<WeakSet<RegExp>>>({} as WeakSet<RegExp>);
expectType<UndefinableDeep<WeakSet<symbol>>>({} as WeakSet<symbol>);
expectType<UndefinableDeep<WeakSet<Date | {a: number}>>>({} as WeakSet<Date | {a: number | undefined}>);
expectType<UndefinableDeep<WeakSet<never>>>({} as WeakSet<never>);
expectType<UndefinableDeep<WeakMap<never, string>>>({} as WeakMap<never, string | undefined>);
expectType<UndefinableDeep<WeakSet<() => void>>>({} as WeakSet<() => void>);

// Promise containers remain present, while primitive fulfillment values widen.
expectType<UndefinableDeep<Promise<string>>>({} as Promise<string | undefined>);
expectType<UndefinableDeep<Promise<{a: number}>>>({} as Promise<{a: number | undefined}>);
expectNotAssignable<UndefinableDeep<{a: Promise<string>}>>({a: undefined});

// Callable leaves retain their complete signatures and attached properties.
type FunctionWithProperties = {(a1: string, a2: number): boolean; p1: string; readonly p2: {q: number}};
expectType<UndefinableDeep<FunctionWithProperties>>({} as FunctionWithProperties | undefined);
expectType<UndefinableDeep<(value: string) => number>>({} as ((value: string) => number) | undefined);
type GenericFunction = {<Value>(value: Value): Value; description: string};
declare const genericFunction: UndefinableDeep<GenericFunction>;
expectType<GenericFunction | undefined>(genericFunction);
if (genericFunction) {
expectType<'literal'>(genericFunction('literal'));
expectType<123>(genericFunction(123));
expectType<string>(genericFunction.description);
}

type OverloadedFunction = {(value: string): string; (value: number): number; description: string};
declare const overloadedFunction: UndefinableDeep<OverloadedFunction>;
expectType<OverloadedFunction | undefined>(overloadedFunction);
if (overloadedFunction) {
expectType<string>(overloadedFunction('value'));
expectType<number>(overloadedFunction(1));
expectType<string>(overloadedFunction.description);
}

// Concrete, abstract, and generic constructors are leaves too.
type Constructor = new (value: string) => {value: string};
type AbstractConstructor = abstract new (value: string) => {value: string};
type GenericConstructor = {description: string; new<Value>(value: Value): {value: Value}};
expectType<UndefinableDeep<Constructor>>({} as Constructor | undefined);
expectType<UndefinableDeep<AbstractConstructor>>({} as AbstractConstructor | undefined);
declare const GenericConstructorValue: UndefinableDeep<GenericConstructor>;
expectType<GenericConstructor | undefined>(GenericConstructorValue);
if (GenericConstructorValue) {
expectType<{value: string}>(new GenericConstructorValue('value'));
expectType<string>(GenericConstructorValue.description);
}

// An impossible leaf can be cleared; unknown and any retain their existing breadth.
expectType<UndefinableDeep<never>>(undefined);
expectType<UndefinableDeep<{a: never}>>({} as {a: undefined});
expectType<UndefinableDeep<never[]>>({} as undefined[]);
expectType<UndefinableDeep<unknown>>({} as unknown);
expectType<UndefinableDeep<any>>({} as any);
Loading