expect-type icon indicating copy to clipboard operation
expect-type copied to clipboard

[feature] `.toEqualUnorderedTypeOf`

Open gomain opened this issue 2 years ago • 5 comments

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

gomain avatar Mar 21 '23 13:03 gomain

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.

mmkal avatar Apr 08 '23 03:04 mmkal

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 avatar Apr 08 '23 10:04 gomain

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

gomain avatar Apr 08 '23 10:04 gomain

@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 avatar Aug 23 '24 03:08 mmkal