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 8 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 @@ -5,6 +5,7 @@ export * from './source/basic';
export * from './source/observable-like';

// Utilities
export type {DeepUndefinedToNull} from './source/deep-undefined-to-null';
export type {EmptyObject, IsEmptyObject} from './source/empty-object';
export type {Except} from './source/except';
export type {TaggedUnion} from './source/tagged-union';
Expand Down
1 change: 1 addition & 0 deletions readme.md
Expand Up @@ -96,6 +96,7 @@ Click the type names for complete docs.

### Utilities

- [`DeepUndefinedToNull`](source/deep-undefined-to-null.d.ts) - Type function that accepts a record, recursively removes all optional property modifiers, and in those properties' values replaces `undefined` with `null`.
- [`EmptyObject`](source/empty-object.d.ts) - Represents a strictly empty plain object, the `{}` value.
- [`IsEmptyObject`](source/empty-object.d.ts) - Returns a `boolean` for whether the type is strictly equal to an empty plain object, the `{}` value.
- [`Except`](source/except.d.ts) - Create a type from an object type without certain keys. This is a stricter version of [`Omit`](https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys).
Expand Down
122 changes: 122 additions & 0 deletions source/deep-undefined-to-null.d.ts
@@ -0,0 +1,122 @@
/**
* Type function that accepts a record, recursively removes all optional property modifiers, and in those properties' values replaces `undefined` with `null`.
Copy link
Owner

Choose a reason for hiding this comment

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

We don't use the * prefix for doc comments. See other types here.

*
* - 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 `DeepUndefinedToNull` lets you specify the type to replace `undefined` with (otherwise it defaults to null).
*
* 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 `DeepUndefinedToNull`, 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 {DeepUndefinedToNull} from 'type-fest';
*
* type TypeWithOptionalProps = {a?: 1; b: 2; c: 3 | undefined; d: {e?: 3}};
* type TypeWithoutOptionals = DeepUndefinedToNull<TypeWithOptionalProps>;
* // ^? {a: 1 | null; b: 2; c: 3 | undefined; d: {e: 3 | null}}
*
* type NestedUnionWithOptionalProps = {a?: {b?: 1} | {b?: 2}};
* type NestedUnionWithoutOptionals = DeepUndefinedToNull<NestedUnionWithOptionalProps>;
* // ^? {a: null | {b: 1 | null} | {b: 2 | null}}
*
* type TypeWithCustomReplacement = DeepUndefinedToNull<TypeWithOptionalProps, "yolo">;
* // ^? {a: 1 | "yolo"; b: 2; c: 3 | undefined; d: {e: 3 | "yolo"}}
*
* @category Type
* @category Object
*/
export type DeepUndefinedToNull<
Type,
Replace = null,
> = DeepReplace<DeepUndefinedToPlaceholder<Type, Replace>, Placeholder, undefined>;

declare namespace Any {
export type record = Record<symbol, any>;
export type array = readonly any[];
}

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

/**
* This could probably be separated into its own module and exported
*/
type DeepReplace<
Type,
Find,
Replace,
> = Type extends Type
? Type extends Find
? Replace
: Type extends Any.record
? {[Key in keyof Type]: DeepReplace<Type[Key], Find, Replace>}
: Type extends Any.array
? {[Ix in keyof Type]: DeepReplace<Type[Ix], Find, Replace>}
: Type
: never;

type ReplaceUnionMember<
Union,
Find,
Replace,
> = Union extends Union
? Union extends Find
? Replace
: Union
: never;

/** @internal */
type DeepUndefinedToPlaceholder<
Type,
Replace,
>
/**
* `Type extends Type` distributes `Type`, so for the rest of this
* function `Type` refers to the individual member of the union,
* rather than the union itself
*/
= Type extends Type
/**
* Does this member of the union extend `undefined`? Replace it in
* the union it belongs to as `Placeholder`
*/
? Type extends undefined
? Placeholder
/**
* Is `Type` a record?
*/
: Type extends Record<symbol, any>
? {
/**
* Make the properties of `Type` required
*/
[Key in keyof Type]-?:
/**
* Traverse each value recursively
*/
DeepUndefinedToPlaceholder<
/**
* Is `Key` optional in `Type`?
*/
{} extends Pick<Type, Key>
/**
* ...if yes, replace `undefined` with `null` in `Type[Key]` union
*/
? ReplaceUnionMember<Type[Key], undefined, Replace>
/**
* ...otherwise just use `Type[Key]` when recursing
*/
: Type[Key]
, Replace
>
}
: Type
: never;
104 changes: 104 additions & 0 deletions test-d/deep-undefined-to-null.ts
@@ -0,0 +1,104 @@
import {expectType} from 'tsd';
import type {DeepUndefinedToNull} from '../index';

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

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

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?: 3 | undefined;
d?: 4 | null;
e?: 5 | undefined | null;
f: 6 | undefined;
g?: {};
h: {
i: 7;
j?: 7;
k?: {
l?: 8;
m: 9;
n?: 10 | undefined;
o?: 11 | null;
p?: 12 | undefined | null;
q: 13 | undefined;
};
};
};

type Out5 = {
a: 1;
b: | null | 2;
c: | null | 3;
d: | null | 4;
e: | null | 5;
f: | undefined | 6;
g: | null | {};
h: {
i: 7;
j: | null | 7;
k:
| null
| {
l: | null | 8;
m: 9;
n: | null | 10;
o: | null | 11;
p: | 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: 1 | {
b: 2;
c: 3 | null;
d: readonly undefined[] | {
e: {
a: 1 | null;
} | {
a: 2 | null;
} | null;
} | undefined;
} | null;
};

type In7 = {
a?: 1;
};

type Out7 = {
a: | 0 | 1;
};

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

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