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

[feature] .toEqualUnorderedTypeOf #24

Open
gomain opened this issue Mar 21, 2023 · 5 comments
Open

[feature] .toEqualUnorderedTypeOf #24

gomain opened this issue Mar 21, 2023 · 5 comments
Labels
help wanted Extra attention is needed New feature New feature or request

Comments

@gomain
Copy link

gomain commented Mar 21, 2023

I have a use case where the order of items in the construction of a tuple type is non-deterministic.
To test I currently do:

  const tuple = expectTypeOf<TupleType>();
  tuple.toMatchTypeOf<[any, any, any]>(); // has length 3
  tuple.items.exclude<A | B>().toEqualTypeOf<C>(); // one of the items is C
  tuple.items.exclude<A | C>().toEqualTypeOf<B>(); // one of the items is B
  tuple.items.exclude<B | C>().toEqualTypeOf<A>(); // one of the items is A

But this only works when tuple items are indeed not union types. There is no way to test if one of the items type is a certain union type.

I propose a .toEqualUnorderedTypeOf:

  expectTypeOf<TestType>().toEqualUnorderedTypeOf<[A, B, A]>();
  /* any of these would pass, and fail otherwise
   * [A, B, A]
   * [B, A, A]
   * [A, A, B]
   */

Other matchers/combinators along this line are

  • .toContainTypeOf // passes if at least one item has equal type
  • .toContainItemsTypeOf // passes if tuple items is a superset of provided tuple
  • .excludeItem // returns tuple with items of equal type removed, or fail if not found
  • .excludeItems // returns tuple minus (in set sense) the provided tuple, or fail if can't
@mmkal
Copy link
Owner

mmkal commented Apr 8, 2023

These seems a little niche and probably complex to implement - could you say more about the use case? Maybe there's a way it could be made deterministic, it seems a bit strange for types to be non-deterministic.

@gomain
Copy link
Author

gomain commented Apr 8, 2023

There is this trick to implement transforming union types to tuples of constituents:

// must condition on T
type Contra<T> = T extends infer I ? (arg: I) => void : never;

// must wrap in tuple
type InferContra<F> = [F] extends [(arg: infer T) => void] ? T : never;

/*
 * Which one is picked is nondeteriminstic.
 * Recall that "a" | "b" == "b" | "a".
 */
type PickOne<T> = InferContra<InferContra<<Contra<<Contra<T>>>>;

type _Union2Tuple<ACC extends any[], T> = PickOne<T> extends infer U
  ? Exclude<T, U> extends never
    ? [T, ...ACC]
    : _Union2Tuple<[U, ...ACC], Exclude<T, U>>
  : never;

type Union2Tuple<T> = _Union2Tuple<[], T>;

/*
 * Because the order of constructing the tuple type is nondeterministic,
 * we can not assert with a tuple type. We can assert the length of the tuple
 * by _matching_ with tuples such as `[any, any]` to assert a length of 2.
 * Then exclude all but except one type from the tuple items type in all
 * combinations.
 */
{
  const tuple = expectTypeOf<Union2Tuple<"a" | 1>>();
  tuple.toMatchTypeOf<[any, any]>();
  tuple.items.exclude<"a">().toEqualTypeOf<1>();
  tuple.items.exclude<1>().toEqualTypeOf<"a">();
}

@gomain
Copy link
Author

gomain commented Apr 8, 2023

@mmkal I'm willing to hack on how one would be implemented. Names are open for bike-shedding.

@aryaemami59 aryaemami59 added New feature New feature or request help wanted Extra attention is needed labels Mar 19, 2024
@mmkal
Copy link
Owner

mmkal commented Aug 23, 2024

@gomain this is an old issue, but v0.20.0 added a UnionToTuple type for some internal stuff. And you are right, I have observed the ordering of the tuple does seem to be non-deterministic. I assumed it would just be unstable between typescript versions, but it can change between times you hover on a type in the IDE.

If you still want to try it out, and can find a not-too-complicated way to implement, I'd accept a PR.

@mmkal
Copy link
Owner

mmkal commented Oct 9, 2024

For fun I thought I'd try this. It's fairly easy to use the existing utility types to get something that will work for this use case:

type IsSubset<Small extends any[], Big extends any[]> = And<{
  [SmallKey in keyof Small]: Or<{
    [BigKey in keyof Big]: StrictEqualUsingTSInternalIdenticalToOperator<Small[SmallKey], Big[BigKey]>
  }>
}>

type TuplesHaveIdenticalItems<A extends any[], B extends any[]> = And<
  [Extends<A['length'], B['length']>, IsSubset<A, B>, IsSubset<B, A>]
>

tests

expectTypeOf<IsSubset<[1, 2, 3, 4], [1, 2, 3]>>().toEqualTypeOf<false>()
expectTypeOf<IsSubset<[1, 2, 3], [1, 2, 3, 4]>>().toEqualTypeOf<true>()

expectTypeOf<TuplesHaveIdenticalItems<[1, 2, 3], [1, 2, 3, 4]>>().toEqualTypeOf<false>()
expectTypeOf<TuplesHaveIdenticalItems<[1, 2, 3], [1, 2, 3]>>().toEqualTypeOf<true>()
expectTypeOf<TuplesHaveIdenticalItems<[1, 2, 3], [3, 2, 1]>>().toEqualTypeOf<true>()
expectTypeOf<TuplesHaveIdenticalItems<[1, 2, 3], [3, 2, 1, 1]>>().toEqualTypeOf<false>()

expectTypeOf<TuplesHaveIdenticalItems<[1, 1, 2, 2, 3], [1, 1, 1, 2, 3]>>().toEqualTypeOf<true>()

That last one is a little sad, because while they do have the same number of items and the same items, it doesn't feel like [1, 1, 2, 2, 3] is "the same" as [1, 1, 1, 2, 3].


alternate approach: "count" the matches between A and B, and compare them to the count of the matches between A and A:

type CountTrues<Bools extends boolean[], Tally extends string = ''> = Bools extends []
  ? Tally
  : Bools extends [infer Head, ...infer Rest extends boolean[]]
    ? CountTrues<Rest, `${Tally}${Head extends true ? 'I' : ''}`>
    : never

type Matches<A extends any[], B extends any[]> = {
  [Akey in keyof A]: CountTrues<{
    [Bkey in keyof B]: StrictEqualUsingTSInternalIdenticalToOperator<A[Akey], B[Bkey]>
  }>
}
type TuplesHaveIdenticalItems<A extends any[], B extends any[]> = And<[
  Extends<A['length'], B['length']>,
  StrictEqualUsingTSInternalIdenticalToOperator<Matches<A, B>, Matches<A, A>>,
]>

Tests look better!

expectTypeOf<TuplesHaveIdenticalItems<[1, 2, 3], [1, 2, 3, 4]>>().toEqualTypeOf<false>()
expectTypeOf<TuplesHaveIdenticalItems<[1, 2, 3], [1, 2, 3]>>().toEqualTypeOf<true>()
expectTypeOf<TuplesHaveIdenticalItems<[1, 2, 3], [3, 2, 1]>>().toEqualTypeOf<true>()
expectTypeOf<TuplesHaveIdenticalItems<[1, 2, 3], [3, 2, 1, 1]>>().toEqualTypeOf<false>()

expectTypeOf<TuplesHaveIdenticalItems<[1, 1, 2, 2, 3], [1, 1, 1, 2, 3]>>().toEqualTypeOf<false>()
expectTypeOf<TuplesHaveIdenticalItems<[1, 1, 2, 2, 3], [1, 1, 3, 2, 2]>>().toEqualTypeOf<true>()

I'm tempted to put this in think but this might be too niche for this library though!

Playground for those interested

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed New feature New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants