0

I have a higher order function, createHandler, that takes rest arguments in the form of a variadic mapped tuple, mapping them to a generic object type (let's call it ObjA<> for now). The function that is returned can then be passed to another higher order function, useHandler, that will call it (obviously doing more things in the process).

I assert the type of the function that is returned from createHandler is a particular type (just intersecting it with a "brand", i.e. BrandedValidHandler), so that only useHandler can take it and call it.

I'd like to perform some validation on the arguments array to createHandler before accepting it as valid input. Specifically, I'd like to check for duplicate strings in a field of ObjA<>, and reject the input if there's a duplicate. The issue I'm running into is performing the check for duplicates on the argument array to createHandler without losing the generic type inference (specifically, some awesome contextual typing).

Is there a way to perform this check on the inputs without losing this inference?

The solution I've come up with is to validate the inputs in the return type of createHandler instead, and return some type of error instead of BrandedValidHandler. The expectation is createHandler and useHandler will solely be used in conjunction with one another, so this "works", but preferably the inputs themselves would be invalid, instead of the return type. Here is a minimal, fairly contrived, annotated example (I'm not actually checking for duplicate names in User objects, but something like this is the point):


type User<Name extends string, Email extends string> = {
  name: Name;
  email: Email;
  // The handler can use the exact name and email provided above
  handler: (name: Name, email: Email) => void;
};

// Homomorphic mapped tuple allows us infer Names and Emails
type MapUsers<Names extends string[], Emails extends string[]> =
  (
    {
      [K in keyof Names]: User<Names[K], K extends keyof Emails ? Emails[K] : never>
    }
    &
    {
      [K in keyof Emails]: User<K extends keyof Names ? Names[K] : never, Emails[K]>
    }
  );


// a type describing a valid creation of a `createUsers` handler
type ValidUsersFunc = (eventType: 'a' | 'b') => Promise<string> & { __brand: 'ValidUsersFunc' };

// check for duplicate names amongst any of the User objects
type IsUniqueUsersNames<Users extends readonly User<any, any>[]> =
  Users extends readonly [infer U extends User<any, any>, ...infer Rest extends User<any, any>[]]
  ? U['name'] extends Rest[number]['name']
  ? Error & { reason: `Duplicate name: '${U['name']}'` }
  : IsUniqueUsersNames<Rest>
  : ValidUsersFunc;

// Higher order function to return function that can be called by `useUsers`
const createUsers = <Names extends string[], Emails extends string[]>(...users: MapUsers<Names, Emails>) =>
  (async (eventType: 'a' | 'b') => {
    console.log(eventType);
    users.forEach(console.log)
    return "";
  }) as unknown as IsUniqueUsersNames<(typeof users)>; // pretty cool that we can use (typeof users) here though, I might add!

const users = createUsers(
  {
    name: 'conor',
    email: 'conor@example.com',
    handler: (name, email) => {
      name; // contextually typed as 'conor'
      email; // contextually typed as 'conor@example.com'
    }
  },
  {
    name: 'joe',
    email: 'joe@example.com',
    handler: (name, email) => {
      name;
      email;
    }
  },
  {
    name: 'conor', // uh oh, can't have 'conor' twice
    email: 'conor@google.com',
    handler: (name, email) => {
      name;
      email;
    }
  }
);
const useUsers = async (users: ValidUsersFunc) => {
  return await users('a');
};

// ERROR, which is good!
// Argument of type 'Error & { reason: "Duplicate name: 'conor'"; }' is not assignable to parameter of type 'ValidUsersFunc'.
useUsers(users);

What I'd instead like to do is use the mapped type I create in MapUsers, and perform the duplicates check there instead. However, doing something like this loses the generic inference:

type MapUsers<Names extends string[], Emails extends string[]> =
  (
    {
      [K in keyof Names]: User<Names[K], K extends keyof Emails ? Emails[K] : never>
    }
    &
    {
      [K in keyof Emails]: User<K extends keyof Names ? Names[K] : never, Emails[K]>
    }
  ) extends (infer users extends User<any, any>[] // Uh oh, `any`!
  // This creates an issue, using `any` loses all type inference gained above in the mapped tuple intersection (I think).
  ? IsUniqueUsersNames<users> extends true // note: would be a different implementation of `IsUniqueUsersNames`, just returns a boolean instead
  ? users
  : Error
  : never;

Attempt 2: extends infer users extends any[]: loses the contextual typing

Attempt 3: extends infer users: can create a new mapped type without complaints, but then ...users is not recognized as an array in createUsers.

Attempt 4 grasping at straws: loses contextual typing

extends infer users extends User<any, any>[]
  ? {
    [K in keyof users]: users[K] extends User<infer N, infer E> ? User<N, E> : never
  }

What I'm looking for is a way to "store" or "remember" the generics in the infer users line, but I'm not sure that's possible. Obviously the User type cannot exist standalone.

For full transparency, this builds on the foundations I learned from this other question I asked, and using the approach for checking for duplicates in an array from here.

Conor
  • 125
  • 7
  • need minimal example, what is your input and what is your desired output – Acid Coder Dec 24 '22 at 07:45
  • @AcidCoder Apologies, but what part of the example I show could use a clarification? My desired input is any number of `User<>` objects above, and the desired output of `createUsers` is another function. Specifically, `createUsers` is a meant to return an APIGatewayProxyHandler, but I simplified the example to something more broad. – Conor Dec 24 '22 at 13:47
  • I'm hoping the `User<>` object rest parameters could be highlighted for errors as [this answer](https://stackoverflow.com/a/57021889/20071103) shows. The `AsUniqueArray` type the answer shows though needs the _same_ array to be passed to it twice though, and the whole mapped tuple intersection thing in MapUsers would be that array, but obviously it is unwieldy to create that twice, hence my hope that an inferred type parameter could be used: `... extends infer users ...` – Conor Dec 24 '22 at 13:48

1 Answers1

0

Generally, I would use

import {F} from 'ts-toolbelt'

function validate<T extends any[]>(
  ...args: T extends Validate<T> ? T : F.NoInfer<Validate<T>>
): void {}

type Validate<T extends any[]> = Unique<T>

where Validate makes type more or less correct, by replacing bad values with [..., Error, ...] , [..., never, ...] or whatever

Some other question's Playground where I've used that

Dimava
  • 7,654
  • 1
  • 9
  • 24
  • Thanks for the answer @Dimava, unfortunately I'm struggling to see what `F.NoInfer` is doing in this case. Is that saying if the ...args we supply does not extend our `Validate` type, to default to unknown instead? In which case is the "false" branch with `F.NoInfer` hit? Presumably also in the `Validate` type, which uses `Unique`, I'd modify the array as [this answer](https://stackoverflow.com/a/57021889/20071103) does? Does this depend on using an intermediary type, `Validate`, instead of Unique directly? I'm just curious if that pattern is required. How would this work with other generics? – Conor Dec 24 '22 at 14:26
  • I'm not sure my use case needs deferred type inference, as [this question](https://stackoverflow.com/questions/56687668/a-way-to-disable-type-argument-inference-in-generics) did. In fact, as the [answer](https://stackoverflow.com/a/74884027/20071103) to my question about getting the inference to work at all explains, the compiler is trying to infer `Names` and `Emails` from the intersection members of `MapUsers`, and would that inference break by using `F.NoInfer`? Apologies for all the questions, but this is a pretty esoteric use case just trying to understand your approach a little more! – Conor Dec 24 '22 at 14:33
  • @Conor `NoInfer` collapses the type in smth like `args: T extends W ? T : any` which somehow collapses into `args: T` which allows TS to infer `T` as `typeof args` – Dimava Dec 24 '22 at 15:11
  • If the type would be just `args: Validate` T may be not inferred from invalid examples in some cases, leading to badly understandable `function requires 0 arguments` error – Dimava Dec 24 '22 at 15:14