Skip to content

Add UndefinableDeep type - #1498

Open
maximilliangrand wants to merge 2 commits into
sindresorhus:mainfrom
maximilliangrand:add-undefinable-deep-type
Open

maximilliangrand wants to merge 2 commits into
sindresorhus:mainfrom
maximilliangrand:add-undefinable-deep-type

Conversation

@maximilliangrand

@maximilliangrand maximilliangrand commented Aug 14, 2026

Copy link
Copy Markdown

Closes #612.

Adds UndefinableDeep for a fully shaped object whose individual values can be cleared without removing its required containers or properties:

type Draft = UndefinableDeep<{
	textEditor: {fontSize: number};
	tags: string[];
}>;
// {textEditor: {fontSize: number | undefined}; tags: Array<string | undefined>}

The transformation widens leaf values consistently in objects, arrays, tuples, maps, sets and promises, while preserving optional/readonly modifiers. Functions and constructors are leaves, so generic/overloaded signatures and attached properties stay intact rather than being rebuilt with Parameters and ReturnType. Weak-collection keys stay valid by excluding undefined; a never leaf becomes undefined.

This follows the leaf-value contract requested in review. Tests cover required-container rejection, optional and variadic tuples, collections, weak keys, generic/overloaded callables and constructors, and never/unknown/any. README and API documentation describe the same contract.

Validation: the revised contract tests fail against the previous implementation. Full TypeScript compilation and tsd pass, as do XO for the changed files and all 38 custom-linter tests. npm test fails on four XO errors in unchanged test-d/abstract-class.ts and test-d/readonly-deep.ts; all four reproduce in a separate checkout of the previous PR head using the same dependencies. No unrelated lint fixes are included. AI-assisted implementation and independent source review.

@sindresorhus

Copy link
Copy Markdown
Owner

I think this needs some more thought

The issue points to ts-essentials' DeepUndefinable, but this behaves differently: primitive values inside arrays, maps, sets, promises, and tuples are not widened, while an object-valued property itself becomes undefined. Neither contract is necessarily wrong, but it conflicts with the documented "every key" and "every level" behavior. For the fully-shaped object use-case, the referenced behavior seems more coherent: keep object containers present and make their leaf values undefinable.

The type itself makes sense because PartialDeep makes keys optional. I would simplify this around that referenced contract and treat functions and constructors as leaves. Rebuilding callable objects with Parameters<T> and ReturnType<T> also erases generic signatures, turning a generic identity function into effectively (value: unknown) => unknown. Passing callables through avoids this bug and removes the fragile function-specific machinery, which is seperate from the main object-data use-case.

@maximilliangrand

Copy link
Copy Markdown
Author

Reworked this around the leaf-value contract you described. Required object containers now stay present, while primitive leaves widen consistently inside arrays, tuples, maps, sets and promises. A single homomorphic mapping preserves object and tuple modifiers.

Functions and constructors pass through as leaves with undefined added; their generic and overloaded signatures and attached properties remain intact. This removes the Parameters/ReturnType reconstruction and the function-specific helper machinery. Weak-collection keys exclude undefined to satisfy their key constraint.

Added negative container-shape tests and generic/overloaded callable and constructor regressions, alongside collection and tuple coverage. The revised contract fails against the previous implementation. README and API documentation now match the behavior.

Full tsc and tsd pass, along with XO for the changed files and all 38 custom-linter tests. npm test reports four XO errors in unchanged test-d/abstract-class.ts and test-d/readonly-deep.ts; the same four reproduce in a separate checkout of the previous PR head with identical dependencies. AI-assisted implementation and independent source review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Would be good to have DeepUndefinable

2 participants