GithubHelp home page GithubHelp logo

unionize's People

Contributors

dependabot[bot] avatar karol-majewski avatar lorefnon avatar oliverjash avatar pelotom avatar sledorze avatar tvald avatar twop avatar wmaurer avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

unionize's Issues

Union of generic variants

It would be great if unionize could support union of generic variants

I'm using a generic interface in one of my projects to represent generic fetch status

export interface Fetch<T> {
  results: T[]
  finished: boolean
}

and I want it to become

export interface FetchLoading {
  type: 'loading'
}

export interface FetchSuccess<T> {
  type: 'success'
  results: T[]
  finished: boolean
}

export interface FetchSuccessWithErrors<T> {
  type: 'successWithErrors'
  results: T[]
  finished: boolean
  errors: string[]
}

export interface FetchError {
  type: 'error'
  reason: string
}

And I would create an union type which could be consumed by a UI component showing the fetch status according to its type.

How could we achieve that with unionize?

Renaming tags doesn't propagate to `Action.match` keys

I think this can be boiled down to a TypeScript issue. In the following example, foo is not renamed within match, but it should be:

{
  type MyActions = { foo: {}; bar: {} };
  type Cases = Record<keyof MyActions, {}>;
  const match = (cases: Cases) => {}

  match({
    foo: 1,
    bar: 2,
  })
}

If Record is replaced with mapped types, renaming does work:

{
  type MyActions = { foo: {}; bar: {} };
  type Cases = { [key in keyof MyActions]: {} };
  const match = (cases: Cases) => {}

  match({
    foo: 1,
    bar: 2,
  })
}

I opened an issue on TypeScript to track this microsoft/TypeScript#20272.

Filing here in case other people find the same problem when using unionize!

(And thanks for the fantastic library.)

Tags are not removed from variant value

import { ofType, unionize } from 'unionize';

const MyUnion = unionize({
    MyVariant: ofType<{ foo: string }>(),
});
type MyUnion = typeof MyUnion._Union;

const myUnion = MyUnion.MyVariant({ foo: 'bar' });
MyUnion.match(myUnion, {
    // Type: { foo: 'bar' }
    // Expected logged value: { foo: 'bar' } (same as type)
    // Actual logged value: { tag: 'MyVariant', foo: 'bar' }
    MyVariant: myVariant => console.log(myVariant),
});

Workaround: if we specify a value, we don't have this issue:

const MyUnion = unionize(
    {
        MyVariant: ofType<{ foo: string }>(),
    },
    { value: 'value' },
);

Why this matters: we are spreading the unionize variant value into a React component:

<div {...myVariant} />

We expect the spreaded object to contain valid props for this React component, which it does, however it also passes the tag prop which is not valid. This results in invalid HTML.

IMO the runtime value should match the type. There should not be an excess property containing the tag—although I realise this would mean creating a new object, in order to remove the tag

Match multiple unions simultaneously

In Elm you can match against multiple types at once, by storing them in a tuple and then matching against that:

type Action = AgreeToTerms | AddFiles { files : List File }
type State = Onboarding | Form { files : List File }

myMatch : Action -> State -> State
myMatch action state = case ( action, state ) of
  ( AgreeToTerms, Onboarding ) ->
    Form { files = [] }
  ( AddFiles { files }, Form form ) ->
    Form { files = form.files }
  ( _ , _ ) ->
    state

Full example: https://github.com/rtfeldman/elm-spa-example/blob/17a3398623f9e538f14f5b0d039fd62b3beae934/src/Main.elm#L210

Currently, if we want to do the same via Unionize, the best we can do is nesting matchs:

const Action = unionize({
    AgreeToTerms: ofType<{}>(),
    AddFiles: ofType<{ files: File[] }>(),
});
type Action = UnionOf<typeof Action>;

const State = unionize({
    Onboarding: ofType<{}>(),
    Form: ofType<{ files: File[] }>(),
});
type State = UnionOf<typeof State>;

const myMatch = (action: Action) => (state: State) =>
    Action.match({
        AgreeToTerms: () =>
            State.match({
                Onboarding: () => State.Form({ files: [] }),
                default: () => state,
            })(state),
        AddFiles: ({ files }) =>
            State.match({
                Form: form => State.Form({ files: form.files.concat(files) }),
                default: () => state,
            })(state),
        default: () => state,
    })(action);

It would be amazing if we could somehow provide a syntax for matching mutliple unions simultaneously. Pseudo code:

const myMatch = match([Action, State])([
    [[['AgreeToTerms', 'Onboarding']], () =>
        State.Form({ files: [] })
    ],
    [['AddFiles', 'Form'], ({ files }, form) =>
        State.Form({ files: form.files.concat(files) })
    ],
    [['default', 'default'], (_, state) =>
        state
    ]
])

I have no idea if that would be possible with TS, though. WDYT?

Way to access tags as string constants from unionized [proposal]

Hi 👋
first of all, thanks for this awesome utility library, I am having great time using it.

However, there is one use case which I think could be optimized. I find myself in situations, where I need to get access to the underlying string constants behind my actions/unions.

This leads me to write unions like this, which gets very boilerplateful and verbose pretty fast.

const ON = 'ON';
const OFF = 'OFF';

const State = unionized({
  [ON]: ofType<{}>(),
  [OFF]: ofType<{}>()
})

// do something later with tag
console.log(ON)  // 'ON'

In perfect world, .is, .match and .transform should be enough, but when interfacing with specific libraries the need to access union tags is unavoidable. This in turn requires the user to sacrifice brevity.

Proposal

Store Record<tags, tags> inside the Unionized object.

Usage example:

const State = unionized({
  ON: ofType<{}>(),
  OFF: ofType<{}>()
})

// do something later with tag
console.log(State.tags.ON)  // 'ON'

I will be 100% willing and happy to prepare a PR with that feature, just let me know if you are interested in it and would like to include such functionality into the codebase.

Cheers 😄

API not working

is and match do not provide anything.
typescript version is 2.4.2
I guess it may be related to a problem with the Proxy definition.

Nested unions are "untagged"

I just ran into this bug which resulted in a runtime exception. 😢

import { unionize, ofType, UnionOf } from 'unionize';

const NestedAction = unionize({ Foo: {} }, { value: 'value' });
type NestedAction = UnionOf<typeof NestedAction>;

const Action = unionize({ Nested: ofType<NestedAction>() }, { value: 'value' });

declare const wrong: { value: {} };

// No error!!!
Action.Nested(wrong);

// Expected: (value: { tag: "Foo"; } & { value: {}; }) => …
// Actual: (value: Pick<{ tag: "Foo"; } & { value: {}; }, "value">) => …
// Why is `tag` omitted??
Action.Nested;

One known workaround is to force the nested union tag to be different:

-const NestedAction = unionize({ Foo: {} }, { value: 'value' });
+const NestedAction = unionize({ Foo: {} }, { tag: 'tag2', value: 'value' });

However that's a non-option when modelling Redux actions, since the tag must be type to conform to the usual Redux action type.

Error using Unionize in angular 6.0.0

ERROR in node_modules/unionize/lib/index.d.ts(17,29): error TS1005: ';' expected.
node_modules/unionize/lib/index.d.ts(17,66): error TS1005: '(' expected.
node_modules/unionize/lib/index.d.ts(17,104): error TS1005: ']' expected.
node_modules/unionize/lib/index.d.ts(17,116): error TS1005: ',' expected.
node_modules/unionize/lib/index.d.ts(17,119): error TS1005: ';' expected.
node_modules/unionize/lib/index.d.ts(17,182): error TS1005: ']' expected.
node_modules/unionize/lib/index.d.ts(17,194): error TS1005: ';' expected.
node_modules/unionize/lib/index.d.ts(17,195): error TS1128: Declaration or statement expected.
node_modules/unionize/lib/index.d.ts(18,1): error TS1128: Declaration or statement expected.
node_modules/unionize/lib/index.d.ts(43,36): error TS1005: ';' expected.
node_modules/unionize/lib/index.d.ts(44,23): error TS1005: ';' expected.
node_modules/unionize/lib/index.d.ts(45,7): error TS1128: Declaration or statement expected.
node_modules/unionize/lib/index.d.ts(45,19): error TS1005: ';' expected.
node_modules/unionize/lib/index.d.ts(46,23): error TS1005: ';' expected.
node_modules/unionize/lib/index.d.ts(47,7): error TS1109: Expression expected.
node_modules/unionize/lib/index.d.ts(48,1): error TS1128: Declaration or statement expected.
node_modules/unionize/lib/index.d.ts(49,79): error TS1005: ';' expected.
node_modules/unionize/lib/index.d.ts(49,90): error TS1128: Declaration or statement expected.
node_modules/unionize/lib/index.d.ts(50,17): error TS1005: ']' expected.
node_modules/unionize/lib/index.d.ts(50,23): error TS1005: ',' expected.
node_modules/unionize/lib/index.d.ts(50,24): error TS1136: Property assignment expected.
node_modules/unionize/lib/index.d.ts(50,28): error TS1005: ':' expected.
node_modules/unionize/lib/index.d.ts(50,36): error TS1005: ':' expected.
node_modules/unionize/lib/index.d.ts(50,55): error TS1005: ',' expected.
node_modules/unionize/lib/index.d.ts(51,9): error TS1005: ']' expected.
node_modules/unionize/lib/index.d.ts(51,15): error TS1005: ';' expected.
node_modules/unionize/lib/index.d.ts(51,16): error TS1109: Expression expected.
node_modules/unionize/lib/index.d.ts(51,18): error TS1109: Expression expected.

ℹ 「wdm」: Failed to compile.

Suggestion: require value property to avoid collisions when nesting

When a union is nested inside a union, without a value property specified, the two values will collide:

E.g.

https://stackblitz.com/edit/typescript-ef4lbo?file=index.ts

import { unionize, ofType } from 'unionize';

const InsideFoo = unionize({
  Bar: ofType<{ bar: number }>(),
});
type InsideFoo = typeof InsideFoo._Union;
const A = unionize({
  Foo: ofType<InsideFoo>(),
});

const a = A.Foo(InsideFoo.Bar({ bar: 1 }));

console.log(JSON.stringify(a))

Logs:

{"bar":1,"tag":"Foo"}

As you can see, the tag for the nested union is lost, and the value for the nested union is merged with the value for the parent union.

We can fix this by specifying a value property:

import { unionize, ofType } from 'unionize';

const InsideFoo = unionize({
  Bar: ofType<{ bar: number }>(),
}, { value: 'value' });
type InsideFoo = typeof InsideFoo._Union;
const A = unionize({
  Foo: ofType<InsideFoo>(),
}, { value: 'value' });

const a = A.Foo(InsideFoo.Bar({ bar: 1 }));

console.log(JSON.stringify(a))

Logs:

{"tag":"Foo","value":{"tag":"Bar","value":{"bar":1}}}

For this reason, I think that the value property should be required. I always forget to add one, and then run into these issues. Ideally I wouldn't have to specify a value property each time I use unionize.

Related code:

valProp ? { [tagProp]: tag, [valProp]: value } : { ...value, [tagProp]: tag }) as any;

Deserialising/validating/decoding JSON strings to unionize types

Do you have any suggestions how to validate values as unionize types? Here is a real world example of where this is needed.

I have a union type to represent a page modal.

const Modal = unionize({
    Foo: ofType<{ foo: number }>(),
    Bar: ofType<{ bar: number }>()
});
type Modal = typeof Modal._Union;

The modal is specified to the application through the URL as a query param, e.g. http://foo.com/?modal=VALUE, where VALUE is an object after it has been JSON stringified and URI encoded, e.g.

`?modal=${encodeURIComponent(JSON.stringify(Modal.Foo({ foo: 1 })))}`
// ?modal=%7B%22tag%22%3A%22Foo%22%2C%22foo%22%3A1%7D

In my application I want to be able to match against the modal using the match helpers provide by unionize. However, it is not safe to do so, because the modal query param could be a string containing anything, e.g. modal=foo.

For this reason I need to validate the value before matching against it.

In the past I've enjoyed using io-ts for the purpose of validating. I am aware you also have a similar library called runtypes.

If this is a common use case, I wonder if there's anything we could work into the library, or build on top of it, to make it easier.

Here is a working example that uses io-ts and tries to share as much as possible, but duplication is inevitable and it's a lot of boilerplate.

import { ofType, unionize } from "unionize";
import * as t from "io-ts";
import { option } from "fp-ts";

//
// Define our runtime types, for validation
//

const FooRT = t.type({ tag: t.literal("Foo"), foo: t.number });
type Foo = t.TypeOf<typeof FooRT>;

const BarRT = t.type({ tag: t.literal("Bar"), bar: t.number });
type Bar = t.TypeOf<typeof BarRT>;

const ModalRT = t.taggedUnion("tag", [FooRT, BarRT]);

//
// Define our unionize types, for object construction and matching
//

export const Modal = unionize({
    Foo: ofType<Foo>(),
    Bar: ofType<Bar>()
});
export type Modal = typeof Modal._Union;

//
// Example of using the unionize object constructors
//

const modalFoo = Modal.Foo({ foo: 1 });

//
// Example of validation with io-ts + matching with unionize
//

const parseJsonSafe = (str: string) => option.tryCatch(() => JSON.parse(str));

const validateModal = (str: string) => {
    console.log("validating string:", str);

    parseJsonSafe(str).foldL(
        () => {
            console.log("invalid json");
        },
        parsedJson => {
            ModalRT.decode(parsedJson).fold(
                () => console.log("parsed json, invalid modal"),
                modal =>
                    Modal.match({
                        Foo: foo =>
                            console.log("parsed json, valid modal foo", foo),
                        Bar: bar =>
                            console.log("parsed json, valid modal bar", bar)
                    })(modal)
            );
        }
    );
};

validateModal(JSON.stringify({ tag: "Foo", foo: 1 }));
/*
validating string: {"tag":"Foo","foo":1}
parsed json, valid modal foo { tag: 'Foo', foo: 1 }
*/
validateModal(JSON.stringify({ tag: "Bar", bar: 1 }));
/*
validating string: {"tag":"Bar","bar":1}
parsed json, valid modal bar { tag: 'Bar', bar: 1 }
*/
validateModal("INVALID JSON TEST");
/*
validating string: INVALID JSON TEST
invalid json
*/

Suggestion on inline usage, updates, simple records and "_" prop as a default

I started to use unionize at work for modelling redux states. So far it is a great conceptual fit however we miss several features/syntax :)

Usually it looks similar to this.

const State = unionize({
  Loading: ofType<{}>(), // note there is no payload
  Loaded: ofType<{ data: string }>(),
  Error: ofType<{ err: string }>()
});

Pattern matching

So far we have way more inline usage of matching (when there is an object to match with right away).
default syntax is a bit verbose ()=>{...}
payload for default case is not the initial object (useful for updates)

Suggestion

const val = State.Loaded({ data: 'some data' });
const str = State.match(val, { // note that val is a fist arg here
  Loaded: ({ data }) => data,
  _: v => 'default' // default, v === val here
});

"_" is a reserved prop for default case. Which is aligned with other languages + probably won't be used (vs "def" or "default")

In summary

  • "_" is a fallback case + the original object is passed as payload so it is possible to write {_:identity} where identity = x=>x

  • inline usage vs curried. Maybe another func name: matchInl, matchI or even switch :)

First class support for no payload cases

const State = unionize({
  Loading: simple(), // need a better name though, "noArgs" maybe?
  Loaded: ofType<{ data: string }>(),
  Error: ofType<{ err: string }>()
});

const loading = State.Loading(); // note no args to create it.
//So it can even be memoized (return the same object all the time)

Immutable update

this comes up a lot with dealing with redux. I need to modify data only if state is loaded.

const updatedVal = State.update(val, {
  Loaded: ({ data }) => ({ data: data + ' yay!' })
});

Note:

  • update signature:
    update:: State -> State.
  • If Loaded case would have a different shape ofType<{data: string, count: number}> nothing would change. So Loaded case has a signature of
    Loaded:: Loaded -> Partial Loaded (similar to react's "setState").
  • There is no need to provide Error and Loading cases. The same value is returned if there is no match.
    Functionally will be similar to prism.

config object as an argument

I know that in ur example it was used to represent redux actions but writing

const Action = unionize({
  ADD_TODO:  ofType<{ id: string; text: string }>(),
}, 'type', 'payload');

const action = Action.ADD_TODO({...})

is a bit tedious.

suggestion

const Todos = unionize(
  { Add: ofType<{ id: string; text: string }>() },
  { tag: 'type', payload: 'payload', prefix: '[TODOS] ' }
);

type Config = { tag?: string; payload?: string; prefix?: string };

//so you can simplify creating new actions
const namespace = (prefix: string) => ({ tag: 'type', payload: 'payload', prefix })

const Todos = unionize(
  { Add: ofType<{ id: string; text: string }>() },
  namespace( '[TODOS] ')
);

type AddType = typeof Todos._Record.Add
// {tag: "[TODOS] Add", payload: {id: string; text: string}}

I know this is a lot of suggestions but I decided to reach out to see if they make sense to you :)

And thanks for an amazing library!

Allow parameter-less element factory when ofType<void> is specified

When specifying an element with ofType<void>, it was my expectation that I could then use the element factory without specifying an argument, e.g. AuthActions.LOGOUT(). Instead it turns out I have to do AuthActions.LOGOUT(null) or AuthActions.LOGOUT(undefined), which feels rather unnecessary. It seems to me like there should be a way to enable this behavior, by having two different types for the factory depending on whether the type is void or not.

Matching multiple with single case

Currently one has to duplicate cases:

export const getGridCountForDevice = Device.match({
  Phone: () => GridCount.Two({}),
  Tablet: () => GridCount.Two({}),
  Desktop: () => GridCount.Three({}),
});

Or use defaults:

export const getGridCountForDevice = Device.match({
  Desktop: () => GridCount.Three({}),
}, () => GridCount.Two({}));

It would be fantastic if unionize had a way of pattern matching multiple tags with a single case:

export const getGridCountForDevice = Device.match([
  [['Tablet', 'Phone'], () => GridCount.Two({})],
  ['Desktop', () => GridCount.Three({})],
]);

Although I imagine this would complicate the behind-the-scenes types of match quite drastically.

Truly "empty" type constructors? (Disallow passing arguments)

Hi, I was recently playing around with unionize and discovered that using {} to represent members of the union which take no arguments doesn't actually prevent you from passing arguments through in the functions passed to match via the type constructors. I think, ideally, you would receive a type error when trying to pass any argument at all due to that being the intention of modeling a simple, "empty" type.

Do you have any ideas for how this could be made to work? Is it possible?

UnionOf does not work.

it generates a variance issue (typescript 2.8.3).

Code

  describe('unionOf', () => {
    it('should be usable', () => {
      const T = unionize(
        {
          foo: ofType<{ x: number }>(),
        }
      );
      type ActionType = UnionOf<typeof T> // Error message here
    });
  });

Error message:

[ts]
Type 'Unionized<{ foo: { x: number; }; }, MultiValueVariants<{ foo: { x: number; }; }, "tag">>' does not satisfy the constraint 'Unionized<any, any>'.
  Type 'Unionized<{ foo: { x: number; }; }, MultiValueVariants<{ foo: { x: number; }; }, "tag">>' is not assignable to type '{ _Tags: string; _Record: any; _Union: any; is: Predicates<any>; as: Casts<any, any>; match: Matc...'.
    Types of property 'match' are incompatible.
      Type 'Match<{ foo: { x: number; }; }, { tag: "foo"; } & { x: number; }>' is not assignable to type 'Match<any, any>'.
        Types of parameters 'cases' and 'cases' are incompatible.
          Type 'MatchCases<any, any, any>' is not assignable to type 'MatchCases<{ foo: { x: number; }; }, { tag: "foo"; } & { x: number; }, any>'.
            Type 'Cases<any, any> & NoDefaultProp' is not assignable to type 'MatchCases<{ foo: { x: number; }; }, { tag: "foo"; } & { x: number; }, any>'.
              Type 'Cases<any, any> & NoDefaultProp' is not assignable to type 'Partial<Cases<{ foo: { x: number; }; }, any>> & { default: (variant: { tag: "foo"; } & { x: numbe...'.
                Type 'Cases<any, any> & NoDefaultProp' is not assignable to type '{ default: (variant: { tag: "foo"; } & { x: number; }) => any; }'.
                  Types of property 'default' are incompatible.
                    Type 'undefined' is not assignable to type '(variant: { tag: "foo"; } & { x: number; }) => any'.

The pattern used by UnionOf works well with interfaces but not with types (variance handling is not the same I guess).

Union cannot be made an interface because it mixes Creators which cannot be made an interface.

One possible solution would be to nest Creators inside a field of Union, like so:

export interface Unionized<Record, TaggedRecord> {
  _Tags: keyof TaggedRecord;
  _Record: Record;
  _Union: TaggedRecord[keyof TaggedRecord];
  is: Predicates<TaggedRecord>;
  as: Casts<Record, TaggedRecord[keyof TaggedRecord]>;
  match: Match<Record, TaggedRecord[keyof TaggedRecord]>;
  transform: Transform<Record, TaggedRecord[keyof TaggedRecord]>;
  create: Creators<Record, TaggedRecord>
}

(obviously you need to change the way creators are mixed into the related function)
In that case it works, however that's a breaking change.

Constructors holding `Partial<T>` can be called with any argument

When using Partial as a value type the calling the constructors with any object type checks but it should not. For example the following should not type check

type T = { field: string }

const U = unionize({
  PartialT: ofType<Partial<T>>(),
})

// This should fail
const u = U.PartialT({ foo: true })
const u2 = U.PartialT(true)

The underlying problems seems to be that the constraint {} extends UnTagged<Partial<T>, 'tag'> does not hold. If we look at the definition of Creators we see that this results in the constructor accepting an argument of type undefined | {} which seems wrong.

export type Creators<Record, TaggedRecord, TagProp extends string> = {
  [T in keyof Record]: {} extends UnTagged<Record[T], TagProp>
    ? ((value?: {}) => TaggedRecord[keyof TaggedRecord])
    : ((value: UnTagged<Record[T], TagProp>) => TaggedRecord[keyof TaggedRecord])
}

I’m not sure what the solution would be but would be happy to create a PR if somebody has a suggestion.

how I use with @ngrx?

I tried to use but, I do not know how to do to reducer funcion. In the example of the readme, it defines a const variable, but in @ngrx, needed a funcion

A question about Type predicates

In the traditional way, in the effects, it's used actions$.ofType(), In your proposal
action$.filter(Action.is.TOGGLE_TODO). my question is, your proposal does the same? It is faster than the traditional way?

Creating action with undefined payload converts to {}

I just ran into this issue after upgrading to 2.x, which has caused a serious bug in our application.

Using an action creator without specifying a payload, for an action that expects a payload (created with typeOf), results in the payload being "autogenerated" as the empty object {}. This is a serious issue because sometimes we want to pass on the fact that there is no value being passed.

We managed to work around the issue by converting to null when calling the action creator, e.g. MyActionType.DoSomething(value || null). However this seems to be an unnecessary burden for developers to always have to do this.

Ability to define each variant individually

Enabling creating several unions from some variants

const success = ofType<AddActionSuccess>()
const failure = ofType<AddActionFailure>()
const error = ofType<AddActionError>()

export const addAction = unionize({
  success,
  failure
})

export const addAction2 = unionize({
  success,
  failure,
  error
})

and also using the predicates and constructor from each variant, individually (while preserving usage from the union too!)

const success = ofType<AddActionSuccess>()
success.is(...) // predicate
success.of(...) // constructor

Type Params

We cannot express the code below currently (because of Type Params):

type Success<T> = { type: 'success', value: T }
const makeSuccess = <T>(value: T): Result<T> => ({ type: 'success', value });
const isSuccess = <T>(r: Result<T>): r is Success<T> => r.type === 'success';

type Failure = { type: 'failure' }
const makeFailure = <T>(): Result<T> => ({ type: 'failure' });
const isFailure = <T>(r: Result<T>): r is Failure => r.type === 'failure';

type Result<T> = Success<T> | Failure;

It may not be possible with the current API (Not spent any time thinking about it).
However for the record, let put that here.

Missing error when value contains property matching `TagProp`

When a value contains a property of the same name as TagProp, when using the constructor for that value, I expect an error if I forget to pass that property, but I don't:

import { ofType, unionize } from 'unionize';
const ContentMode = unionize({
    Content: ofType<{ tag: string }>(),
});
// no error, what's going on?!!
ContentMode.Content();
import { ofType, unionize } from 'unionize';
const ContentMode = unionize({
    Content: ofType<{ myTag: string }>(),
}, { tag: 'myTag' });
// no error, what's going on?!!
ContentMode.Content();

In some ways this makes sense due to the fact Unionize does not nest the value by default, so these two properties will actually collide (#46).

However, even if I enable value nesting, I still get no error:

import { ofType, unionize } from 'unionize';
const ContentMode = unionize({
    Content: ofType<{ tag: string }>(),
}, { value: 'value' });
// no error, what's going on?!!
ContentMode.Content();

Non-object record types are converted to objects

import { ofType, unionize, UnionOf } from 'unionize';

const MyUnion = unionize({
    MyVariant: ofType<string[]>(),
});
type MyUnion = UnionOf<typeof MyUnion>;

MyUnion.match({
    MyVariant: strs => strs.map(x => x), // runtime error!
});

Related: #67

Workaround: specify a value:

 const MyUnion = unionize({
     MyVariant: ofType<string[]>(),
-});
+}, { value: 'value' });

Update README.md

We need README.md updated to reflect the changes implemented in the latest release. Please this is very important to keep everything well documented.

<3 unionize!

UX issues when inspecting types (truncation, type aliases not used)

We're using Unionize pretty heavily at Unsplash (thank you!). I would like to share some UX feedback relating to how the union types appear when inspected. To do so, I will use an example.

Vanilla tagged union

First let's see how type inspection works for a sans-Unionize union:

type Foo = { foo: number };
type Bar = { bar: number };
type Baz = { baz: number };

type MyNestedUnion =
  | {
      tag: "NestedFoo";
      value: Foo;
    }
  | {
      tag: "NestedBar";
      value: Bar;
    }
  | {
      tag: "NestedBaz";
      value: Baz;
    };

// Hover this
type MyUnion =
  | {
      tag: "Foo";
      value: Foo;
    }
  | {
      tag: "Bar";
      value: Bar;
    }
  | {
      tag: "Baz";
      value: Baz;
    }
  | {
      tag: "Union";
      value: MyNestedUnion;
    };

// Hover this
declare const fn: (f: MyUnion) => void;

Hover over type MyUnion and you'll see:

type MyUnion =
  | {
      tag: "Foo";
      value: Foo;
    }
  | {
      tag: "Bar";
      value: Bar;
    }
  | {
      tag: "Baz";
      value: Baz;
    }
  | {
      tag: "Union";
      value: MyNestedUnion;
    };

Great! Hover over const fn and you'll see:

const fn: (f: MyUnion) => void;

Great! No problems here.

Unionize union

Now if we define the equivalent tagged union but using Unionize:

import { ofType, unionize, UnionOf } from "unionize";

type Foo = { foo: number };
type Bar = { bar: number };
type Baz = { baz: number };

const MyNestedUnion = unionize(
  {
    NestedFoo: ofType<Foo>(),
    NestedBar: ofType<Bar>(),
    NestedBaz: ofType<Baz>(),
  },
  { value: "value" },
);
type MyNestedUnion = UnionOf<typeof MyNestedUnion>;

const MyUnion = unionize(
  {
    Foo: ofType<Foo>(),
    Bar: ofType<Bar>(),
    Baz: ofType<Baz>(),
    Union: ofType<MyNestedUnion>(),
  },
  { value: "value" },
);
// Hover this
type MyUnion = UnionOf<typeof MyUnion>;

// Hover this
declare const fn: (f: MyUnion) => void;

Now hover over type MyUnion and you'll see:

type MyUnion = ({
    tag: "Foo";
} & {
    value: Foo;
}) | ({
    tag: "Bar";
} & {
    value: Bar;
}) | ({
    tag: "Baz";
} & {
    value: Baz;
}) | ({
    tag: "Union";
} & {
    value: ({
        tag: "NestedFoo";
    } & {
        value: Foo;
    }) | ({
        tag: "NestedBar";
    } & {
        ...;
    }) | ({
        ...;
    } & {
        ...;
    });
})

Observations:

  1. {
        tag: "Foo";
    } & {
        value: Foo;
    }
    … could be simplified to
    {
      tag: "Foo";
      value: Foo;
    }
    Is this something TS should do automatically for us? In the interim, is this something we can workaround, e.g. using Compact (which "cleans" types with lots of arithmetic)?
  2. In the sans-Unionize example, the MyNestedUnion type alias was shown nested inside of here. However in this Unionize example, the type alias MyNestedUnion is thrown away and the whole type structure is shown instead. Why? Is this a design limitation or bug in TS? Is it something we can workaround?
  3. Some types are truncated. This makes these types really difficult to work with. Often times, inspecting a type is not enough to understand how to work with it—instead you have to look deep into the definition. If we can't fix 2 (doing so would negate this), can we prevent truncation somehow?

Hover over const fn and you'll see:

const fn: (f: ({
    tag: "Foo";
} & {
    value: Foo;
}) | ({
    tag: "Bar";
} & {
    value: Bar;
}) | ({
    tag: "Baz";
} & {
    value: Baz;
}) | ({
    tag: "Union";
} & {
    value: ({
        tag: "NestedFoo";
    } & {
        value: Foo;
    }) | ({
        tag: "NestedBar";
    } & {
        ...;
    }) | ({
        ...;
    } & {
        ...;
    });
})) => void

In the sans-Unionize example, the MyUnion type alias was shown here. However in this Unionize example, the type alias MyUnion is thrown away and the whole type structure is shown instead. Why?

I appreciate there might not be much we can do from Unionize's side to help address these UX issues. However I imagine there's at least some discussions inside of TS we could chime into, so our voice is heard. 🤞

How to define Action without payload?

How to define Action Without payload?

I have actions with payload and without payload In my definition.
With payload is: ADD_TODO: ofType<{ id: string; text: string }>()

but without How is it?

I tried this: CLEAN: ofType<{}>() but the compiler show error when call store.dispatch(Action.CLEAN())
It works, but the compiler shows me error.
What is the right way?

Compatibility with Flux Standard Actions (multiple top-level value/payload keys)

While testing the viability of using unionize for actions & reducers in an existing Redux app, I discovered that, despite being able to alias tag as type and value as payload, to provide a bit of Redux-compatibility, there is no real way to handle being able to add error and meta keys on the same level as payload.

You could, of course, lump those keys under your value/payload property and then use some extra mapping step to extract those things out & create an FSA style action object, but that takes away from a lot of the convenience & nicety of using a solution like unionize.

Is there anyway around this?

Somewhat unrelated, but I'm also curious about the viability of approaching a library like this that doesn't rely on records but instead allows the type constructors for each member of the union to accept multiple arguments. I'm thinking along the lines of something like the way daggy works, but typed with TS. Any thoughts on this? Any caveats that would make it very difficult/not possible?

Enumerate union

I want to create a tuple of all my union's members.

To achieve this with unions elsewhere, I can do the following:

type ExactMatch<Union, Arr> = Union[] extends Arr ? (Arr extends Union[] ? Arr : never) : never;
const enumerateUnion = <T extends unknown>() => <U extends unknown>(
  arg: ExactMatch<T, U>,
) => arg;

As recommended here: microsoft/TypeScript#13298 (comment)

For example:

enum Foo {
  bar,
  baz
}

// good
enumerateUnion<Foo>()([Foo.bar, Foo.baz])
// bad
enumerateUnion<Foo>()([Foo.bar])

However, when I tried to do this with a unionize union, I realised it's not possible because the constructors return the union rather than the (tagged) member that has been constructed:

const Foo = unionize({
  bar: {},
  baz: {}
});

// good (should be bad!)
enumerateUnion<Foo>()([Foo.bar()])

Have you got any alternative ideas about how we could enumerate a unionize union? Alternatively, is there a way to make unionize union's work with the enumerateUnion I defined above?

Circular definition issue

const def = unionize({
  or: ofType<{ a: Def, b: Def }>()
})

type Def = typeof def._Union // Error: Type alias 'Def' circularly references itself

One cannot create a recursive Union.

More precise typing of variants.

When instantiating one variant of a union, it is typed as the type of the union.
It would be nicer to have the actual narrower type of the variant.

To achieve this, one could change the signature of Creators from

export type Creators<Record, TaggedRecord> = {
  [T in keyof Record]: (value: Record[T]) => TaggedRecord[keyof TaggedRecord]
}

to

export type Creators<Record, TaggedRecord> = {
  [T in (keyof Record & keyof TaggedRecord)]: (value: Record[T]) => TaggedRecord[T]
}

any chance to get that in?

Creating subsets from existing unions

Is it possible to create a subset union type from an existing union type?

E.g. given type Foo = A | B | C | D, I want to create another union that is just composed of A | B.

Perhaps this is a bad idea and I'm looking at this the wrong way. For context, here is why I think I need this.

I have this union:

const PropertyComparison = unionize({
    Same: ofType<{}>(),
    SimilarFunctions: ofType<{}>(),
    ValuesDeepEqual: ofType<{}>(),
    Unequal: ofType<PropertyComparisonUnequal>(),
});
type PropertyComparison = typeof PropertyComparison._Union;

And this record type:

type PropertiesComparison = { [key: string]: PropertyComparison };

Now I also want to create a type like PropertiesComparison except where the values can only be ValuesDeepEqual | SimilarFunctions:

type PropertyComparisonSubset = PropertyComparison.ValuesDeepEqual | PropertyComparison.SimilarFunctions;
type PropertiesComparisonSubset = { [key: string]: PropertyComparisonSubset };

Documentation(?): Way to filter multiple types

For libraries such as redux-observable or @ngrx/effects it's a regular requirement to filter a stream of actions, which can be done with unionize like so:

this.actions$.pipe(
    filter(DomainActions.is.MY_DOMAIN_ACTION),
    ...
  );

and most of the time this is enough. However, sometimes it's necessary to filter according to multiple action types. At the moment this could be done with something like the following:

/** Example 1 **/

this.actions$.pipe(
    filter(action => 
      DomainActions.is.MY_DOMAIN_ACTION(action as DomainActions._Union) ||
      AnotherDomainActions.is.ANOTHER_DOMAIN_ACTION(action as AnotherDomainActions._Union))
    ...
  );

or perhaps:

/** Example 2 **/

this.actions$.pipe(
    filter(testMultiple(
      DomainActions.is.MY_DOMAIN_ACTION
      AnotherDomainActions.is.ANOTHER_DOMAIN_ACTION
    )),
    ...
  );

function testMultiple(...predicates: ((a: any) => boolean)[]) {
  return (a: any) => predicates.some(predicate => predicate(a));
}

Using something like the ngrx ofType operator this could be written as:

/** Example 3 **/

this.actions$.pipe(
    ofType(
      DomainActions.MY_DOMAIN_ACTION.name,
      AnotherDomainActions.ANOTHER_DOMAIN_ACTION.name
    ),
    ...
  );

but unfortunately the name property is undefined so I can't do that. 😦

So my question is, @pelotom or anyone else who might have an idea, what is the recommendation for such a case? Should I just create my own testMultiple() utility function à la example 2? Or is there some way to obtain the tag property as a string so I can do something like example 3? Or might there be some other way?

Match case not infering/enforcing return value of function

It appears that the return type of match clauses isn't strongly checked/inferred by the return type of the function.

In this example, you can see that the INCREMENT_COUNT match clause function is not returning a type of StoreState, but no compiler error is thrown. However, the DECREMENT_COUNT clause function explicitly declares that it is returning a StoreState type, and a compiler error is thrown.

Not sure if it is possible to make this return type inferred somehow, but it'd be nice to not have to remember to explicitly declare the return type of each match clause.

P.S. I absolutely love this library, and appreciate all the work that has been done for it. 😄 ❤️

Feature request: Prevent tag name collisions

In order to avoid name collisions for Redux action types it is currently necessary (or at least best practice) to repeat the domain in the type name, for instance:

export const AuthActions = unionize({
  AUTH_LOGIN: ofType<LoginData>(),
  AUTH_LOGIN_SUCCESS: ofType<AuthData>(),
  AUTH_LOGOUT: ofType<void>(),
  AUTH_ERROR: ofType<any>(),
}, {
  tag: 'type',
  value: 'payload',
});

One way to alleviate the issue would be if the type string could be prefixed, for instance:

export const AuthActions = unionize({
  LOGIN: ofType<LoginData>(),
  LOGIN_SUCCESS: ofType<AuthData>(),
  LOGOUT: ofType<void>(),
  ERROR: ofType<any>(),
}, {
  tag: 'type',
  value: 'payload',
  tagPrefix:  'AUTH_'
});

Consumers could then reference things by their short names, e.g. AuthActions.LOGIN while the actual string behind the scenes would be AUTH_LOGIN, thus avoiding collisions with other non-auth domains.

However, this still wouldn't guarantee that tag name collisions wouldn't occur. For that a registry of tags would be necessary as well as a way to configure whether the tag names should be considered unique. The registry could have a structure like

const TAG_REGISTRY: {[tag: string]: string[]} = {
  'type': [
    'AUTH_LOGIN',
    'AUTH_LOGIN_SUCCESS',
    'AUTH_LOGOUT',
    'AUTH_ERROR',
  ]
};

and the configuration whether a tag should be considered unique could be done by an option unique: true or similar:

{
  tag: 'type',
  value: 'payload',
  tagPrefix:  'AUTH_',
  unique: true
};

Then if a tag name for the same tag (e.g. type) were to be used twice, an error could be thrown, thus preventing accidental action type collisions.

Provide means to Simplify inferred types

Unionize is very useful and lean.

one can write easily such things:

      const T = unionize({
        foo: ofType<{x : number}>(),
      });
      const input = { x: 42 };
      expect(T.foo(input).tag).toBe('foo');

the inferred type for foo is then:

{
    tag: "foo";
} & {
    x: number;
}

However, as a union grows the signature start to be verbose get in the way, so one can use interface to keep things clearer.

      interface Foo {
        x: number;
      }
      const T = unionize({
        foo: ofType<Foo>(),
      });
      const input = { x: 42 };
      expect(T.foo(input).tag).toBe('foo');

which generates that:

{
    tag: "foo";
} & Foo

It's halfway to totally removing the boilerplate if one want to go further by doing so:

      interface Foo {
        tag: 'foo';
        x: number;
      }
      const T = unionize({
        foo: ofType<Foo>(),
      });
      const input = { x: 42, tag: 'foo' as 'foo' };
      expect(T.foo(input).tag).toBe('foo');

which generates that:

Foo

with (a bit) more effort we can come to this:

      interface Foo {
        tag: 'foo';
        x: number;
      }
      const T = unionize({
        foo: ofType<Foo>(),
      });
      const input = { x: 42 };
      expect(T.foo(input).tag).toBe('foo');

which still generates that:

Foo

I may provide a PR if interested

NPM?

when can be ready in NPM?

Proposal: restore old API of creating union via type parameter

While we're making breaking changes for 2.0, I've been thinking about ways we could return to the simpler original API of Unionize. It used to be that the unionize function used a type parameter instead of an actual object to specify the mapping of tags to values:

const Action = unionize<{
  ADD_TODO: { id: string; text: string }
  SET_VISIBILITY_FILTER: 'SHOW_ALL' | 'SHOW_ACTIVE' | 'SHOW_COMPLETED'
  TOGGLE_TODO: { id: string }
}>('type', 'payload')

Simple and clean!

The magic that powered this was ES6 Proxys; unfortunately it was pointed out that proxies carried an enormous performance penalty, and thus we ended up with the current API involving ofType with its impossible signature of <A>() => A. It works and is efficient, but it's unsightly and unsafe.

How could we get back to something like what we had originally, but without using Proxy? Well, we only needed it to trap property accesses in support of the creation, is and as functions:

Action.ADD_TODO({ id: 'abc', text: 'hello world' });
Action.is.ADD_TODO(obj);
Action.as.ADD_TODO(obj);

But we could alternatively make these functions which take a tag argument:

Action.create('ADD_TODO', { id: 'abc', text: 'hello world' });
Action.is('ADD_TODO', obj);
Action.as('ADD_TODO', obj);

There could be curried versions of these as well, so that

Action.create('ADD_TODO'); // returns a creation function
Action.is('ADD_TODO'); // returns a type guard
Action.as('ADD_TODO'); // returns a casting function

Slightly more verbose, but not much. The more I think about it, the more this tradeoff seems worth it to leave ofType behind.

Suggestion: add type level tests

It's came up a few times that we don't currently have a way to test that the types behave as expected.

If someone was to have a go at doing this (e.g. me), which tool should be used for the job?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.