简体   繁体   中英

Combining generic function argument types

I want typescript to accept a list of arguments that have similar structures but with disparate values and infer correctly the resulting union type.

Sample code

interface User<
    N extends string = string,
    S extends unknown = unknown,
    A extends (a: S) => unknown = (a: S) => unknown
> {
    name: N;
    state: S;
    action: A;
}

function createUser<
    N extends string = string,
    S extends unknown = unknown,
    A extends (a: S) => unknown = (a: S) => unknown
>(name: N, state: S, action: A): User<N, S, A> {
    return { action, name, state };
}

function combineUsers<
    V extends User<string, unknown, (a: unknown) => unknown>[]
>(...users: V) {
    type Names = V[number]["name"];
    return {
        actions: users.reduce(
            (a, u) => ({ ...a, [u.name]: u.action }),
            {} as { [K in Names]: Extract<V[number], { name: K }>["action"] },
        ),
        names: new Set<Names>(users.map((u) => u.name)),
    };
}

const userA = createUser("user1", 1, (_a: number) => undefined);
const user2 = createUser("userB", "two", (_s: string) => true);
const users = combineUsers(userA, user2);
// Argument of type 'User<"user1", number, (_a: number) => undefined>' is not assignable to parameter of type 'User<string, unknown, (a: unknown) => unknown>'.
//   Type '(_a: number) => undefined' is not assignable to type '(a: unknown) => unknown'.
//     Types of parameters '_a' and 'a' are incompatible.
//       Type 'unknown' is not assignable to type 'number'.ts(2345)

That is the compilation error I get because of the incompatible functions.

What I would expect to see as type returned by the function is some inferred union like;

type Return = {
    names: Set<"user1" | "userB">,
    actions: {
        user1: (_a: number) => undefined;
        userB: (_s: string) => boolean;
    }
}
  • Is this even possible? Can sprinkle any but I'd rather not
  • Are there any alternatives or better ways of achieving this

EDIT:

The solution by @jcalz worked great for the problem as stated in the previous version but my sample was missing something to make it applicable to my actual code, ie when the argument of a function must match a type defined elsewhere.

The typing I'd give to combinedUsers looks like this:

function combineUsers<V extends User<string, never[], unknown>[]>(...users: V) {
  type Names = V[number]['name'];
  return {
    actions: users.reduce(
      (a, u) => ({ ...a, [u.name]: u.action }),
      {} as { [K in Names]: Extract<V[number], { name: K }>['action'] }
    ),
    names: new Set<Names>(users.map((u) => u.name)),
  };
}

Note that there's just one generic type parameter, V , corresponding to the array of User<N, A, R> objects passed in. The type parameters string (covariant upper bound constraint for property types), never[] (contravariant lower bound constraint for argument types), and unknown (covariant upper bound constraint for return value types) are slightly more type safe than any , any , any , but could still allow some weird inputs. Hopefully that doesn't actually matter though.

Inside the implementation we define Names type to grab the union of N parameters for each element of V ( V[number]['name'] is the type you get when you index into users with a number index and then index into the resulting User with a "name" index).

The type of the actions property a mapped type where for each name in Names we find the element(s) of V that have that as their name and get its action type.


Let's see if it works. Assuming you have a userA and user2 of these types:

//const userA: User<"user1", [x: number, y: string], undefined>
//const user2: User<"userB", [x: string], boolean>

Then you combineUsers(userA, user2) results in this:

const users = combineUsers(userA, user2);
/* const users: {
    actions: {
        user1: (x: number, y: string) => undefined;
        userB: (x: string) => boolean;
    };
    names: Set<"user1" | "userB">;
} */

Looks good.

Playground link to code

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