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 all 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
2 changes: 2 additions & 0 deletions index.d.ts
Expand Up @@ -24,6 +24,7 @@ export type {RequireOneOrNone} from './source/require-one-or-none';
export type {OmitIndexSignature} from './source/omit-index-signature';
export type {PickIndexSignature} from './source/pick-index-signature';
export type {PartialDeep, PartialDeepOptions} from './source/partial-deep';
export type {Patch} from './source/patch';
export type {RequiredDeep} from './source/required-deep';
export type {PickDeep} from './source/pick-deep';
export type {PartialOnUndefinedDeep, PartialOnUndefinedDeepOptions} from './source/partial-on-undefined-deep';
Expand Down Expand Up @@ -128,6 +129,7 @@ export type {Join} from './source/join';
export type {Split} from './source/split';
export type {Trim} from './source/trim';
export type {Replace} from './source/replace';
export type {ReplaceDeep} from './source/replace-deep';
export type {Includes} from './source/includes';
export type {Get} from './source/get';
export type {LastArrayElement} from './source/last-array-element';
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Expand Up @@ -121,6 +121,8 @@ 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.
- [`Patch`](source/patch/index.d.ts) - Type constructor that "patches" optional properties. Users can specify which type to use as a replacement; defaults to `undefined`. Depth is configurable; defaults to shallow.
- [`ReplaceDeep`](source/replace-deep.d.ts) - Type constructor that recursively replaces an arbitrary type inside a larger one.
- [`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
10 changes: 10 additions & 0 deletions source/patch/index.d.ts
@@ -0,0 +1,10 @@
import {type Config} from './patch';

Check failure on line 1 in source/patch/index.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 20

'./patch' imported multiple times.

Check failure on line 1 in source/patch/index.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 18

'./patch' imported multiple times.

Check failure on line 1 in source/patch/index.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 16

'./patch' imported multiple times.
import type * as P from './patch';

Check failure on line 2 in source/patch/index.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 20

'./patch' imported multiple times.

Check failure on line 2 in source/patch/index.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 18

'./patch' imported multiple times.

Check failure on line 2 in source/patch/index.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 16

'./patch' imported multiple times.

export * as Patch from './patch';
export type Patch<
TreeRoot,
UserConfig extends
| Config.options
= Config.default,
> = P.Patch<TreeRoot, UserConfig>;
181 changes: 181 additions & 0 deletions source/patch/patch.d.ts
@@ -0,0 +1,181 @@
export {
type Patch,
type Config,
type Example

Check failure on line 4 in source/patch/patch.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 20

Missing trailing comma.

Check failure on line 4 in source/patch/patch.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 18

Missing trailing comma.

Check failure on line 4 in source/patch/patch.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 16

Missing trailing comma.
}

Check failure on line 5 in source/patch/patch.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 20

Missing semicolon.

Check failure on line 5 in source/patch/patch.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 18

Missing semicolon.

Check failure on line 5 in source/patch/patch.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 16

Missing semicolon.

/** @internal */
declare namespace Local {
/** @internal */
export type PLACEHOLDER = typeof PLACEHOLDER

Check failure on line 10 in source/patch/patch.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 20

Missing semicolon.

Check failure on line 10 in source/patch/patch.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 18

Missing semicolon.

Check failure on line 10 in source/patch/patch.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 16

Missing semicolon.
/** @internal */
export const PLACEHOLDER: unique symbol

Check failure on line 12 in source/patch/patch.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 20

Missing semicolon.

Check failure on line 12 in source/patch/patch.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 18

Missing semicolon.

Check failure on line 12 in source/patch/patch.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 16

Missing semicolon.
/** @internal */
export type URI = typeof URI

Check failure on line 14 in source/patch/patch.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 20

Missing semicolon.

Check failure on line 14 in source/patch/patch.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 18

Missing semicolon.

Check failure on line 14 in source/patch/patch.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 16

Missing semicolon.
/** @internal */
export const URI: unique symbol

Check failure on line 16 in source/patch/patch.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 20

Missing semicolon.

Check failure on line 16 in source/patch/patch.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 18

Missing semicolon.

Check failure on line 16 in source/patch/patch.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 16

Missing semicolon.
/** @internal */
export interface ReplaceFn {

Check failure on line 18 in source/patch/patch.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 20

Use a `type` instead of an `interface`.

Check failure on line 18 in source/patch/patch.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 18

Use a `type` instead of an `interface`.

Check failure on line 18 in source/patch/patch.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 16

Use a `type` instead of an `interface`.
[URI]: never

Check failure on line 19 in source/patch/patch.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 20

Expected a semicolon.

Check failure on line 19 in source/patch/patch.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 18

Expected a semicolon.

Check failure on line 19 in source/patch/patch.d.ts

View workflow job for this annotation

GitHub Actions / Node.js 16

Expected a semicolon.
input: unknown
output: unknown
}
/** @internal */
export type Run<Fn extends ReplaceFn, Arg>
= (Fn & { input: Arg })["output"]

/** @internal */
export type Spread<L, R>
= { [K in keyof L]-?: K extends keyof R ? {} extends R[K] ? Exclude<R[K], undefined> : R[K] : L[K] }
}

declare namespace Config {
export {
Options as options,
New as new,
Default as default,
ReplaceFn,
}

type ReplaceFn<fn extends Local.ReplaceFn = Local.ReplaceFn> = fn
type Options<t extends Partial<Config.new> = Partial<Config.new>> = t
type Default = Config.new<undefined, 1>
interface New<
replaceWith = unknown,
maxDepth extends number = number
> {
replaceWith: replaceWith
maxDepth: maxDepth
}
}

declare namespace Example {
type ExampleConfig = Config.new<Example.MaybeFn, -1>
type Maybe<T> = Nothing | Just<T>
interface Nothing { tag: "Nothing" }
interface Just<T> { tag: "Just"; value: T }
interface MaybeFn extends Config.ReplaceFn {
output: Maybe<this["input"]>
}
}

/**
* {@link Patch}
*
* @example
* import type { Patch } from "typefest"
*
* type Equals<T, U> = [T, U] extends [U, T] ? "✅" : "🚫"
*
* // Imported to demo the `replaceWith` option (see example #3)
* import Maybe = Patch.Example.Maybe
* import MaybeFn = Patch.Example.MaybeFn
*
* //////
* // EXAMPLE #1:
* type DemoShallowPatch = Equals<
* // ^? type DemoShallowPatch = "✅"
* Patch<{
* a: { b: 1 }
* f?: { g: 2 }
* j: { k?: 3 }
* m?: { n?: 4 }
* }>,
* {
* a: { b: 1 }
* f: { g: 2 } | undefined
* j: { k?: 3 }
* m: { n?: 4 } | undefined
* }
* >
*
* //////
* // EXAMPLE #2:
* type DemoDeepPatch = Equals<
* // ^? type DemoDeepPatch = "✅"
* Patch<
* {
* a: { b: 1 }
* f?: { g: 2 }
* j: { k?: 3 }
* m?: { n?: 4 }
* },
* { maxDepth: never, replaceWith: null }
* >,
* {
* a: { b: 1 }
* f: { g: 2 } | null
* j: { k: 3 | null }
* m: { n: 4 | null } | null
* }
* >
*
* //////
* // EXAMPLE #3:
* type DemoPatchWithCustomReplace = Equals<
* // ^? type DemoPatchWithCustomReplace = "✅"
* Patch<
* {
* a: { b: 1 }
* f?: { g: 2 }
* j: { k?: 3 }
* m?: { n?: 4 }
* },
* { maxDepth: -1, replaceWith: MaybeFn }
* >,
* (
* {
* a: { b: 1 }
* f: Maybe<{ g: 2 }>
* j: { k: Maybe<3> }
* m: Maybe<{ n: Maybe<4> }>
* }
* )
* >
*/
type Patch<
TreeRoot,
UserConfig extends
| Config.options
= Config.default
>
= unknown extends TreeRoot ? unknown
: Local.Spread<Config.default, UserConfig> extends
| Config.options<infer config> ?
(
ApplyPatch<
Plug<[], config["maxDepth"] & {}, TreeRoot>,
config["replaceWith"]
>
)
: never
;

type Plug<
Depth extends void[],
MaxDepth extends number,
Tree
> = Depth["length"] extends MaxDepth ? Tree
: Tree extends object
? { [ix in keyof Tree]
-?:
Plug<[...Depth, void], MaxDepth, (
{} extends Pick<Tree, ix>
? (Local.PLACEHOLDER | Exclude<Tree[ix], undefined>)
: Tree[ix]
)>
}
: Tree
;

type ApplyPatch<
tree,
replaceWith
>
= Local.PLACEHOLDER extends tree
? [replaceWith] extends [Config.ReplaceFn]
? Local.Run<replaceWith, ApplyPatch<Exclude<tree, Local.PLACEHOLDER>, replaceWith>>
: replaceWith | ApplyPatch<Exclude<tree, Local.PLACEHOLDER>, replaceWith>
: tree extends object ? { [ix in keyof tree]: ApplyPatch<tree[ix], replaceWith> }
: tree
;
39 changes: 39 additions & 0 deletions source/replace-deep.d.ts
@@ -0,0 +1,39 @@
type Query = {needle: unknown; replaceWith: unknown};

declare namespace Local {
/** @internal */
export type URI = typeof URI;
/** @internal */
export const URI: unique symbol;
/** @internal */
export type Run<Fn extends Config.ReplaceFn, Arg>
= (Fn & {input: Arg})['output'];
/** @internal */
export type Primitive =
| string
| number
| boolean
| symbol
| null
| undefined
| bigint;
}

declare namespace Config {
export type ReplaceFn = {
[Local.URI]: never;
input: unknown;
output: unknown;
};
}

export type ReplaceDeep<haystack, query extends Query>
= query extends {needle: infer needle; replaceWith: infer replaceWith}
? [haystack] extends [needle]
? [replaceWith] extends [Config.ReplaceFn]
? Local.Run<replaceWith, Extract<haystack, needle>>
: replaceWith | Exclude<haystack, needle>
: haystack extends Local.Primitive ? haystack
: {[ix in keyof haystack]: ReplaceDeep<haystack[ix], query>}
: haystack;