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

Creating subsets from existing unions #16

Closed
OliverJAsh opened this issue Feb 2, 2018 · 9 comments
Closed

Creating subsets from existing unions #16

OliverJAsh opened this issue Feb 2, 2018 · 9 comments

Comments

@OliverJAsh
Copy link
Collaborator

OliverJAsh commented Feb 2, 2018

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 };
@pelotom
Copy link
Owner

pelotom commented Feb 2, 2018

You could define PropertyComparisonSubset first:

const PropertyComparisonSubset = unionize({
  SimilarFunctions: ofType<{}>(),
  ValuesDeepEqual: ofType<{}>(),
});

and then use it in the definition of PropertyComparison:

const PropertyComparison = unionize({
  Same: ofType<{}>(),
  ...PropertyComparisonSubset._Record,
  Unequal: ofType<PropertyComparisonUnequal>(),
});

@wmaurer
Copy link
Contributor

wmaurer commented Apr 6, 2018

From my testing, this works from the point of view of typings, but it doesn't work at runtime.
_Record is not a property returned from the unionize function:

  return Object.assign({
    is,
    as,
    match,
  }, creators)

Since _Record is undefined, in the above example, the object spread ...PropertyComparisonSubset._Record does not do anything. The result is that calling PropertyComparison.SimilarFunctions({}) will cause an error PropertyComparison.SimilarFunctions is not a function.

I thinking changing the above return statement should fix this problem:

  return Object.assign({
    is,
    as,
    match,
    _Record: record
  }, creators)

@pelotom What do you think? Would you accept a PR for this?

@pelotom
Copy link
Owner

pelotom commented Apr 6, 2018

@wmaurer ah, yes, it should return the _Record that was passed in. Happy to take a PR.

@pelotom pelotom closed this as completed in 328437c Apr 6, 2018
pelotom added a commit that referenced this issue Apr 6, 2018
Return _Record that was passed in. Fixes #16
@OliverJAsh
Copy link
Collaborator Author

OliverJAsh commented Jan 2, 2019

Unfortunately this workaround doesn't help when you want to create multiple (potentially overlapping) subsets, e.g. (pseudocode):

type All = Bob | Sarah | Sam;

type LikesCheese = Pick<All, 'Bob' | 'Sarah'>
type IsOld = Pick<All, 'Sarah'>;

@OliverJAsh
Copy link
Collaborator Author

I find myself needing this again and again. As I mentioned in my previous comment, the workaround mentioned doesn't help, because there can be many overlapping subsets.

It would be really useful to be able to create an existing unionize union and "pick" from it, which would create a new unionize union (type, match, et al).

@wmaurer
Copy link
Contributor

wmaurer commented Jul 2, 2019

@OliverJAsh
The Record that you pass to unionize (and what you get back via the _Record property is simply just an object literal. You can destructure it as you like or use something like Ramda's pick to take what you like and then spread the results into another object literal that you can pass to unionize.
It probably wouldn't take much to create an abstraction that works neatly for your use case.

But I could see problems combining existing unionize unions - e.g. what if they had differring tag or value props?

@OliverJAsh
Copy link
Collaborator Author

Hmm, but if we do that, I don't understand how it's possible to match a subset union. Using my example above:

type All = Bob | Sarah | Sam;

type LikesCheese = Pick<All, 'Bob' | 'Sarah'>
type IsOld = Pick<All, 'Sarah'>;

How would we define a function that checks whether an All instance is a LikesCheese? We would probably have to write one manually 🤔

@pelotom
Copy link
Owner

pelotom commented Jul 14, 2019

Something like this?

// from lodash, ramda or whatever
declare function pick<O, K extends keyof O>(o: O, ...keys: K[]): Pick<O, K>;

const All = unionize({ Bob: {}, Sarah: {}, Sam: {} });
type All = UnionOf<typeof All>;

const LikesCheese = unionize(pick(All, 'Bob', 'Sarah'));
type LikesCheese = UnionOf<typeof LikesCheese>;

// maybeLikesCheese: (all: All) => LikesCheese | null
const maybeLikesCheese = All.match({ ...LikesCheese, default: () => null });

@OliverJAsh
Copy link
Collaborator Author

Interesting. I just tried that but ran into this problem:

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

declare function pick<O, K extends keyof O>(o: O, ...keys: K[]): Pick<O, K>;

const All = unionize({ Bob: ofType<{ name: string }>() });
type All = UnionOf<typeof All>;

const LikesCheese = unionize(pick(All, 'Bob'));
type LikesCheese = UnionOf<typeof LikesCheese>;

// Expected: (value: { name: string }) =>
// Actual: (value?: {} | undefined) =>
LikesCheese.Bob;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants