Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds type constructors Patch and ReplaceDeep #648

Open
wants to merge 38 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
041d6ec
test: write type-level tests for `DeepUndefinedToNull`
ahrjarrett Jul 14, 2023
b6a5487
feat: adds `DeepUndefinedToNull` function
ahrjarrett Jul 14, 2023
9311bd9
chore: adds `DeepUndefinedToNull` to toplevel readme
ahrjarrett Jul 14, 2023
7d7a540
chore: fixes formatting
ahrjarrett Jul 14, 2023
83f6ab4
feat: `DeepUndefinedToNull` supports unions
ahrjarrett Jul 14, 2023
1efc09f
chore: formatting stuff
ahrjarrett Jul 15, 2023
d502f5e
fix: adds `declare` to `Any` namespace
ahrjarrett Jul 15, 2023
2b1f903
fix: revert fomatting
ahrjarrett Jul 15, 2023
ae125e5
makes requested changes, changes the default replacement from `null` …
ahrjarrett Aug 8, 2023
fb1a959
renames file from `deep-undefined-to-null.d.ts` to `patch.d.ts`
ahrjarrett Aug 8, 2023
47f9dd5
finishes renaming `DeepUndefinedToNull` to `Patch`, updates tests to …
ahrjarrett Aug 8, 2023
208f005
renames test file
ahrjarrett Aug 8, 2023
110f996
formats PR
ahrjarrett Aug 8, 2023
4ae8d67
Merge branch 'main' into @ahrjarrett/deep-undefined-to-null
ahrjarrett Aug 8, 2023
1d0683a
chore: renames `Patch` to `ReplaceOptionalDeep`
ahrjarrett Aug 10, 2023
3a1e06c
chore: updates readme, formats test file
ahrjarrett Aug 10, 2023
b2a128f
Merge branch '@ahrjarrett/deep-undefined-to-null' of https://github.c…
ahrjarrett Aug 10, 2023
4e4d552
chore: formats test file
ahrjarrett Aug 10, 2023
9b97f93
Merge branch 'main' into @ahrjarrett/deep-undefined-to-null
sindresorhus Nov 8, 2023
4a1eb46
implements first iteration of `Patch`
ahrjarrett Nov 20, 2023
8910507
moves `patch` into own folder for better types at the call site; adds…
ahrjarrett Nov 20, 2023
5deeae2
adds `Patch` to index.d.ts
ahrjarrett Nov 20, 2023
0587b30
Merge branch 'main' of https://github.com/sindresorhus/type-fest into…
ahrjarrett Nov 20, 2023
267d997
Merge branch '@ahrjarrett/deep-undefined-to-null' of https://github.c…
ahrjarrett Nov 20, 2023
0e210d3
adds `ReplaceDeep` iteration
ahrjarrett Nov 20, 2023
c97f517
exports `ReplaceDeep` from barrel file, cleanup in `patch`, `replace-…
ahrjarrett Nov 20, 2023
2a593da
adds `patch` test to make sure replacing an optional prop with a type…
ahrjarrett Nov 20, 2023
7f7ffa1
adds test to `replace-deep` that boxes the input type -- because the …
ahrjarrett Nov 20, 2023
5856a2f
Merge branch 'main' of https://github.com/sindresorhus/type-fest into…
ahrjarrett Jan 7, 2024
40e3dce
clean up the api to be more explicit
ahrjarrett Jan 7, 2024
9f5d941
removes union check, `isUnion` helper
ahrjarrett Jan 7, 2024
0624d85
removes replace-optional-deep
ahrjarrett Jan 7, 2024
286ec90
fixes patch test
ahrjarrett Jan 7, 2024
adc111d
updates readme and removes export for `ReplaceOptinalDeep`
ahrjarrett Jan 7, 2024
d90faf9
adds `Patch` examples
ahrjarrett Jan 7, 2024
28de8d2
chore: linting errors in `patch/index.d.ts`
ahrjarrett Jan 7, 2024
a2b1723
removes inline test
ahrjarrett Jan 7, 2024
be15e00
autofix `replace-deep.d.ts` and `replace-deep.ts`
ahrjarrett Jan 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions index.d.ts
Expand Up @@ -17,6 +17,7 @@ export type {WritableDeep} from './source/writable-deep';
export type {Merge} from './source/merge';
export type {MergeDeep, MergeDeepOptions} from './source/merge-deep';
export type {MergeExclusive} from './source/merge-exclusive';
export type {ReplaceOptionalDeep} from './source/replace-optional-deep';
export type {RequireAtLeastOne} from './source/require-at-least-one';
export type {RequireExactlyOne} from './source/require-exactly-one';
export type {RequireAllOrNone} from './source/require-all-or-none';
Expand Down
1 change: 1 addition & 0 deletions readme.md
Expand Up @@ -121,6 +121,7 @@ Click the type names for complete docs.
- [`MergeDeep`](source/merge-deep.d.ts) - Merge two objects or two arrays/tuples recursively into a new type.
- [`MergeExclusive`](source/merge-exclusive.d.ts) - Create a type that has mutually exclusive keys.
- [`OverrideProperties`](source/override-properties.d.ts) - Override only existing properties of the given type. Similar to `Merge`, but enforces that the original type has the properties you want to override.
- [`ReplaceOptionalDeep`](source/replace-optional-deep.d.ts) - Type function that accepts a record, recursively removes all optional property modifiers, and adds `undefined` (or whatever type you specify) to the value at that property.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be written slightly more succinctly.

- [`RequireAtLeastOne`](source/require-at-least-one.d.ts) - Create a type that requires at least one of the given keys.
- [`RequireExactlyOne`](source/require-exactly-one.d.ts) - Create a type that requires exactly a single key of the given keys and disallows more.
- [`RequireAllOrNone`](source/require-all-or-none.d.ts) - Create a type that requires all of the given keys or none of the given keys.
Expand Down
83 changes: 83 additions & 0 deletions source/replace-optional-deep.d.ts
@@ -0,0 +1,83 @@
/**
Type function that accepts a record, recursively removes all optional property modifiers, and `undefined` (or whatever is passed as the second
argument) is added to the value at that property.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't hard-wrap.


- Note that this replacement also occurs in all union types (this function is distributive).
- Also note that in non-optional union values that include `undefined`, `undefined` is not removed (undefined is only replaced for optional keys).

Optionally, providing the second parameter to `ReplaceOptionalDeep` lets you specify the type to replace `undefined` with (otherwise it defaults to `undefined`).

Use-cases:

- JSON, for example, does not accept `undefined` as a value. If you need to send an object containing undefined over the wire, without a function
like `ReplaceOptionalDeep`, you'd need to write that type by hand.

- Since JavaScript runtimes will implicitly insert `undefined` in the absence of a value, using `undefined` can create ambiguity (is this value
undefined because someone forgot to add a value, or because `undefined` was used specifically?).

@example
import type {ReplaceOptionalDeep} from 'type-fest';

type TypeWithOptionalProps = {a?: 1; b: 2; c: 3 | undefined; d: {e?: 3}};
type TypeWithoutOptionals = ReplaceOptionalDeep<TypeWithOptionalProps>;
// ^? {a: 1 | undefined; b: 2; c: 3 | undefined; d: {e: 3 | undefined}}

type NestedUnionWithOptionalProps = {a?: {b?: 1} | {b?: 2}};
type NestedUnionWithoutOptionals = ReplaceOptionalDeep<NestedUnionWithOptionalProps, null>;
// ^? {a: null | {b: 1 | null} | {b: 2 | null}}

type TypeWithCustomReplacement = ReplaceOptionalDeep<TypeWithOptionalProps, "yolo">;
// ^? {a: 1 | "yolo"; b: 2; c: 3 | undefined; d: {e: 3 | "yolo"}}

@category Type
@category Object
*/
export type ReplaceOptionalDeep<
Type,
Replacement = undefined,
> = ReplaceDeep<UndefinedToPlaceholderDeep<Type, Replacement>, Placeholder, undefined>;

/** @internal */
type Placeholder = typeof Placeholder;
/** @internal */
declare const Placeholder: unique symbol;

/**
* TODO: Extract `ReplaceDeep` into a separate module and expose from the top-level
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider writing JSDoc now?

type ReplaceDeep<
Type,
Find,
Replace,
> = Type extends Find ? Replace
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bikeshedding comments on the naming here. 😁

Suggestion: "Needle", "Haystack"? They're pretty common terms for search/replace.

Also, "Replace" versus "Replacement"? This project has a "Replace" type already. I wouldn't like to see confusion.

: Type extends Record<symbol, any> ? {[Key in keyof Type]: ReplaceDeep<Type[Key], Find, Replace>}
: Type extends readonly any[] ? {[Ix in keyof Type]: ReplaceDeep<Type[Ix], Find, Replace>}
: Type;

/** @internal */
type UndefinedToPlaceholderDeep<
Type,
Replacement,
>
= Type extends undefined ? Placeholder
// Is `Type` a record?
: Type extends Record<symbol, any>
? {
// Make the properties of `Type` required
[Key in keyof Type]-?:
// Visit each value recursively
UndefinedToPlaceholderDeep<
// Is `Key` optional in `Type`?
{} extends Pick<Type, Key>
// ...if yes, replace `undefined` with `null` in `Type[Key]` union
? ReplaceUnionMember<Type[Key], undefined, Replacement>
// ...otherwise just use `Type[Key]` when recursing
: Type[Key],
Replacement
>
}
: Type
;

/** @internal */
type ReplaceUnionMember<Union, Find, Replace> = Union extends Find ? Replace : Union;
118 changes: 118 additions & 0 deletions test-d/replace-optional-deep.ts
@@ -0,0 +1,118 @@
import {expectType} from 'tsd';
import type {ReplaceOptionalDeep} from '../index';

type In1 = {a: string; b?: boolean};
type Out1 = {a: string; b: undefined | boolean};

type In2 = {a?: string; b?: boolean};
type Out2 = {a: null | string; b: null | boolean};

type In3 = {a: string; b: boolean};
type Out3 = {a: string; b: boolean};

type In4 = {a: undefined};
type Out4 = In4;

type In5 = {
a: 1;
b?: 2;
c?: undefined | 3;
d?: null | 4 ;
e?: undefined | null | 5;
f: undefined | 6;
g?: {};
h: {
i: 7;
j?: 7;
k?: {
l?: 8;
m: 9;
n?: undefined | 10;
o?: null | 11;
p?: undefined | null | 12;
q: undefined | 13;
};
};
};

type Out5 = {
a: 1;
b: undefined | 2;
c: undefined | 3;
d: undefined | null | 4;
e: undefined | null | 5;
f: undefined | 6;
g: undefined | {};
h: {
i: 7;
j: undefined | 7;
k:
| undefined
| {
l: undefined | 8;
m: 9;
n: undefined | 10;
o: undefined | null | 11;
p: undefined | null | 12;
q: undefined | 13;
};
};
};

type In6 = {
a?:
| 1
| {
b: 2;
c?: 3;
d:
| undefined
| readonly undefined[]
| {
e?:
| {a?: 1}
| {a?: 2};
};
};
};

type Out6 = {
a:
| undefined
| 1
| {
b: 2;
c:
| undefined
| 3;
d:
| undefined
| readonly undefined[]
| {
e:
| undefined
| {a: undefined | 1}
| {a: undefined | 2};
};
};
};

type In7 = {a?: 1};

type Out7 = {a: 0 | 1};

declare const test1: ReplaceOptionalDeep<In1>;
declare const test2: ReplaceOptionalDeep<In2, null>;
declare const test3: ReplaceOptionalDeep<In3>;
declare const test4: ReplaceOptionalDeep<In4>;
declare const test5: ReplaceOptionalDeep<In5>;
declare const test6: ReplaceOptionalDeep<In6>;
declare const test7: ReplaceOptionalDeep<In7, 0>;

expectType<Out1>(test1);
expectType<Out2>(test2);
expectType<Out3>(test3);
expectType<Out4>(test4);
expectType<Out5>(test5);
expectType<Out6>(test6);
expectType<Out7>(test7);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Peanut gallery observation: visually verifying this code is going to require scrolling up and down a lot, particularly if this grows more complex in the future. I would recommend moving the declares and expectTypes closer to the types you're actually testing. But if the project owners disagree, I would go with their recommendation.

Not-so-peanut-gallery observation: what happens if someone puts the find type inside the replacement type? Let's come up with some evil replacement testcases too, including:

  • complex types for the replace value
  • never
  • unknown
  • any
  • a value already in the type

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ajvincent thank you for your review.

I appreciate you putting thought into how recursion could misbehave if the replacement itself contained part(s) that matched the needle. The nice thing about implementing find/replace this way (handling a positive match as a base case) is that you avoid that complexity altogether.

I added extra tests for Patch, which I'll be requesting your review on in the next day or 2. But wanted to answer your concern while it was fresh on my mind -- since a match is treated as a base case, and we don't continue recursing, users can use any type they like: any, never, a copy of the entire tree -- because we consider that to be the base case, we will never traverse down that subtree.