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

Added transform function (instead of update) #23

Merged
merged 3 commits into from
Apr 9, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,54 @@ describe('config object', () => {
expect(A.a(1)).toEqual({ tag: 'a', val: 1 });
});
});

describe('transform with value prop', () => {
const Payload = unionize(
{
num: ofType<number>(),
str: ofType<string>(),
},
{ value: 'payload' },
);

it('skips unmet cases', () => {
const num = Payload.num(1);
expect(Payload.transform(num, { str: s => Payload.num(s.length) })).toBe(num);
expect(Payload.transform({ str: s => Payload.num(s.length) })(num)).toBe(num);
});

it('transforms with met cases', () => {
const str = Payload.str('s');
const expected = Payload.num(1);
expect(Payload.transform(str, { str: s => Payload.num(s.length) })).toEqual(expected);
expect(Payload.transform({ str: s => Payload.num(s.length) })(str)).toEqual(expected);
});

it('technically we allow an empty object for cases', () => {
const str = Payload.str('s');
expect(Payload.transform(str, {})).toBe(str);
expect(Payload.transform({})(str)).toBe(str);
});
});

describe('transform without value prop', () => {
const Data = unionize({
num: ofType<{ n: number }>(),
str: ofType<{ s: string }>(),
});

it('Just all at once', () => {
const num = Data.num({ n: 1 });
const str = Data.str({ s: 's' });

const strLen = ({ s }: { s: string }) => Data.num({ n: s.length });

// unmet
expect(Data.transform(num, { str: strLen })).toBe(num);
expect(Data.transform({ str: strLen })(num)).toBe(num);

//met cases
expect(Data.transform(str, { str: strLen })).toEqual(num);
expect(Data.transform({ str: strLen })(str)).toEqual(num);
});
});
36 changes: 28 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type Unionized<Record, TaggedRecord> = {
is: Predicates<TaggedRecord>;
as: Casts<Record, TaggedRecord[keyof TaggedRecord]>;
match: Match<Record, TaggedRecord[keyof TaggedRecord]>;
transform: Transform<Record, TaggedRecord[keyof TaggedRecord]>;
} & Creators<Record, TaggedRecord>;

export type Creators<Record, TaggedRecord> = {
Expand All @@ -30,6 +31,15 @@ export type Match<Record, Union> = {
<A>(variant: Union, cases: MatchCases<Record, Union, A>): A;
};

export type TransformCases<Record, Union> = Partial<
{ [T in keyof Record]: (value: Record[T]) => Union }
>;

export type Transform<Record, Union> = {
(cases: TransformCases<Record, Union>): (variant: Union) => Union;
(variant: Union, cases: TransformCases<Record, Union>): Union;
};

export type MultiValueVariants<Record extends MultiValueRec, TagProp extends string> = {
[T in keyof Record]: { [_ in TagProp]: T } & Record[T]
};
Expand All @@ -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 value is merged into object itself
* @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 getVal = (variant: any) => (valProp ? variant[valProp] : variant);

const creators = {} as Creators<Record, any>;
for (const tag in record) {
creators[tag] = (value: any) =>
Expand All @@ -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(getVal(variant)) : cases.default(variant);
}

const match = (first: any, second?: any) =>
Expand All @@ -107,11 +118,20 @@ export function unionize<Record>(record: Record, config?: { value?: string; tag?
});
}

const evalTransform = (variant: any, cases: any): any => {
const k: keyof Record = variant[tagProp];
return k in cases ? cases[k](getVal(variant)) : variant;
};

const transform = (first: any, second?: any) =>
second ? evalTransform(first, second) : (variant: any) => evalTransform(variant, first);

return Object.assign(
{
is,
as,
match,
transform,
_Record: record,
},
creators,
Expand Down