-
Notifications
You must be signed in to change notification settings - Fork 14
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
Added transform function (instead of update) #23
Changes from 2 commits
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 |
---|---|---|
|
@@ -5,6 +5,7 @@ export type Unionized<Record, TaggedRecord> = { | |
is: Predicates<TaggedRecord>; | ||
as: Casts<Record, TaggedRecord[keyof TaggedRecord]>; | ||
match: Match<Record, TaggedRecord[keyof TaggedRecord]>; | ||
update: Update<Record, TaggedRecord[keyof TaggedRecord]>; | ||
} & Creators<Record, TaggedRecord>; | ||
|
||
export type Creators<Record, TaggedRecord> = { | ||
|
@@ -30,6 +31,15 @@ export type Match<Record, Union> = { | |
<A>(variant: Union, cases: MatchCases<Record, Union, A>): A; | ||
}; | ||
|
||
export type UpdateCases<Record> = Partial< | ||
{ [T in keyof Record]: (value: Record[T]) => Partial<Record[T]> } | ||
>; | ||
|
||
export type Update<Record, Union> = { | ||
(cases: UpdateCases<Record>): (variant: Union) => Union; | ||
(variant: Union, cases: UpdateCases<Record>): Union; | ||
}; | ||
|
||
export type MultiValueVariants<Record extends MultiValueRec, TagProp extends string> = { | ||
[T in keyof Record]: { [_ in TagProp]: T } & Record[T] | ||
}; | ||
|
@@ -55,26 +65,29 @@ export type NoDefaultRec<Val> = { | |
* | ||
* @param record A record mapping tags to value types. The actual values of the record don't | ||
* matter; they're just used in the types of the resulting tagged union. See `ofType`. | ||
* @param tagProp An optional custom name for the tag property of the union. | ||
* @param valProp An optional custom name for the value property of the union. If not specified, | ||
* @param config An optional config object. By default tag='tag' and payload is merged into object itself | ||
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. For consistency, let's say "value" instead of "payload" here. 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. Will do |
||
* @param config.tag An optional custom name for the tag property of the union. | ||
* @param config.value An optional custom name for the value property of the union. If not specified, | ||
* the value must be a dictionary type. | ||
*/ | ||
|
||
export function unionize< | ||
Record extends SingleValueRec, | ||
TagProp extends string, | ||
ValProp extends string | ||
ValProp extends string, | ||
TagProp extends string = 'tag' | ||
>( | ||
record: Record, | ||
config: { value: ValProp; tag?: TagProp }, | ||
): Unionized<Record, SingleValueVariants<Record, TagProp, ValProp>>; | ||
export function unionize<Record extends MultiValueRec, TagProp extends string>( | ||
export function unionize<Record extends MultiValueRec, TagProp extends string = 'tag'>( | ||
record: Record, | ||
config?: { tag: TagProp }, | ||
): Unionized<Record, MultiValueVariants<Record, TagProp>>; | ||
export function unionize<Record>(record: Record, config?: { value?: string; tag?: string }) { | ||
const { value: valProp = undefined, tag: tagProp = 'tag' } = config || {}; | ||
|
||
const payload = (variant: any) => (valProp ? variant[valProp] : variant); | ||
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. s/ 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. I wanted a helper function that will extract value from an object. What about 'extractVal()'? |
||
|
||
const creators = {} as Creators<Record, any>; | ||
for (const tag in record) { | ||
creators[tag] = (value: any) => | ||
|
@@ -89,9 +102,7 @@ export function unionize<Record>(record: Record, config?: { value?: string; tag? | |
function evalMatch(variant: any, cases: any): any { | ||
const k = variant[tagProp]; | ||
const handler = cases[k]; | ||
return handler !== undefined | ||
? handler(valProp ? variant[valProp] : variant) | ||
: cases.default(variant); | ||
return handler !== undefined ? handler(payload(variant)) : cases.default(variant); | ||
} | ||
|
||
const match = (first: any, second?: any) => | ||
|
@@ -107,17 +118,35 @@ export function unionize<Record>(record: Record, config?: { value?: string; tag? | |
}); | ||
} | ||
|
||
const evalUpd = (variant: any, cases: any): any => { | ||
const k: keyof Record = variant[tagProp]; | ||
return k in cases | ||
? creators[k](immutableUpd(payload(variant), cases[k](payload(variant)))) | ||
: variant; | ||
}; | ||
|
||
const update = (first: any, second?: any) => | ||
second ? evalUpd(first, second) : (variant: any) => evalUpd(variant, first); | ||
|
||
return Object.assign( | ||
{ | ||
is, | ||
as, | ||
match, | ||
update, | ||
_Record: record, | ||
}, | ||
creators, | ||
); | ||
} | ||
|
||
// Should we merge objects or just replace them? | ||
// was unable to find a better solution to that | ||
const objType = Object.prototype.toString.call({}); | ||
const isObject = (maybeObj: any) => Object.prototype.toString.call(maybeObj) === objType; | ||
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. Hmm, I'm not sure how reliable or efficient this sort of check is. Does it even make sense to "update" something that's a primitive type? If not, could we statically forbid using this method in that case? 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. I think it does make sense to have update for primitive types (and things like Date, Array etc). But for records (plain object) it makes sense to have partial update. The best reference that I found is react setState signature. They use Pick<K,T> type and not partial. |
||
const immutableUpd = (old: any, updated: any) => | ||
isObject(old) ? Object.assign({}, old, updated) : updated; | ||
|
||
/** | ||
* Creates a pseudo-witness of a given type. That is, it pretends to return a value of | ||
* type `T` for any `T`, but it's really just returning `undefined`. This white lie | ||
|
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.
Using
Partial
for the return type here is unsafe because it allows overwriting values withundefined
. I'm not sure if inference would be able to handle usingPick
though... Perhaps we should statically disallow values to beundefined
, which we're already doing in the case ofSingleValueVariants
.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.
I think it is possible to not use Partial + remove this magic check for 'isObject'.
I think the way to do it is this:
So you either have to provide a complete object (not Partial) or use a helper function.
Not 100% that it is worth complexity. But thinking about updates again, I would either prefer a solution with 'part' function without 'isObject' magic or forbid partial updates. Note that in that case it is still useful: