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.