-
-
Notifications
You must be signed in to change notification settings - Fork 567
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
Changes from 19 commits
041d6ec
b6a5487
9311bd9
7d7a540
83f6ab4
1efc09f
d502f5e
2b1f903
ae125e5
fb1a959
47f9dd5
208f005
110f996
4ae8d67
1d0683a
3a1e06c
b2a128f
4e4d552
9b97f93
4a1eb46
8910507
5deeae2
0587b30
267d997
0e210d3
c97f517
2a593da
7f7ffa1
5856a2f
40e3dce
9f5d941
0624d85
286ec90
adc111d
d90faf9
28de8d2
a2b1723
be15e00
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
*/ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
There was a problem hiding this comment.
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.