简体   繁体   中英

How to validate generic variadic mapped tuple arguments without losing the inferred generic types?

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", ie 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 .

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM