简体   繁体   中英

Infer types in tuple's arrays

Consider, I have a structure, like:

type X = { args: readonly any[]; fn: (...args: any[]) => void }[]

I would like to infer X[number]['args'] in X[number]['fn'] , for example:

[{ args: [1,2,3] as const, fn: (a, b, c) => {} }] // a should be 1, b=2, c=3

In order to do that I create helper function, which should keep constraints

type Arguments<Args> = Args extends any[] ? Args : Args extends readonly any[] ? Args : [];

type InferProviders<Providers> = {
  // Providers[Property] is actually `unknown`. Why?
  [Property in keyof Providers]: Providers[Property] extends { args: infer Args } ? { args: Args, fn: (...args: Arguments<Args>) => any } : never
};

function fn<Providers>(providers: InferProviders<Providers>) {
  return providers;
}

But when I use this function I don't get expected result:

const x = fn([
  { args: [1,2,3], fn: (a, b, c) => {} }, // expected `a` to be 1, b = 2, c = 3
  { args: ['foo', 'bar', 'baz'], fn: (a, b, c) => {} }, // a = 'foo', b = 'bar', c = 'baz'
  { args: ['x', 'y', 'z'], fn: (e, b, c) => {} }, // e = 'x', b ='y',  c = 'z'
] as const); 
// typeof x is [never, never, never]

Playground

Could anyone help how to infer types correctly?

The TypeScript compiler is not really good at inferring both generic type parameters and the contextual type of callback parameters at the same time, if one depends on the other, especially in a single function parameter. This is a design limitation of TypeScript. See microsoft/TypeScript#38872 for a similar issue, and especially this comment which explains what's going on.

The best I could imagine doing for your fn() as written would be to rewrite the typings like this:

type InferProviders<T> = { [K in keyof T]: 
  { args: T[K], fn: (...args: Extract<T[K], readonly any[]>) => any } 
};

function fn<P extends readonly (readonly any[])[]>(
  providers: InferProviders<P>) {
  return providers;
}

And you will see that it compiles with no error and actually produces a value of the right type:

const x = fn([
  { args: [1, 2, 3], fn: (a, b, c) => { } }, 
  { args: ['foo', 'bar', 'baz'], fn: (a, b, c) => { } }, 
  { args: ['x', 'y', 'z'], fn: (e, b, c) => { } }, 
] as const); // okay


/* const x: readonly [{
    args: readonly [1, 2, 3];
    fn: (args_0: 1, args_1: 2, args_2: 3) => any;
}, {
    args: readonly ["foo", "bar", "baz"];
    fn: (args_0: "foo", args_1: "bar", args_2: "baz") => any;
}, {
    args: readonly ["x", "y", "z"];
    fn: (args_0: "x", args_1: "y", args_2: "z") => any;
}] */

But unfortunately, the contextual type inference for the callback parameters happens too late for them to be useful inside the callback implementations themselves:

const x = fn([
  { args: [1, 2, 3], fn: (a, b, c) => { a.oops } }, // a, b, c are any
  { args: ['foo', 'bar', 'baz'], fn: (a, b, c) => { b.oops } }, // a, b, c are any
  { args: ['x', 'y', 'z'], fn: (e, b, c) => { c.oops } }, // e, b, c are any
] as const);

If you're willing to annotate the types of your callback parameters (thereby obviating the need for contextual type inference), then that's fine:

const y = fn([
  { args: [1, 2, 3], 
    fn: (a: 1, b: 2, c: 3) => { a.oops } }, // error!
  { args: ['foo', 'bar', 'baz'], 
    fn: (a: "foo", b: "bar", c: "baz") => { b.toUpperCase() } }, // okay
  { args: ['x', 'y', 'z'], 
    fn: (e: "x", b: "y", c: "z") => { c.toLowerCase() } }, // okay
] as const); // okay

But if you really want contextual type inference, there's not much I can do here, unfortunately, without refactoring the way you call fn() . For example, you could require that someone create providers using a dedicated function where you use separate parameters for args and for fn , so that the compiler has an easier time with inference:

function provider<T extends readonly any[]>(args: T, fn: (...args: T) => any) {
  return { args, fn };
}

and then your call to fn() would be:

const z = fn([
  provider([1, 2, 3] as const, (a, b, c) => a.toFixed()),
  provider(["foo", "bar", "baz"] as const, (a, b, c) => b.toLowerCase()),
  provider([1, 2, 3] as const, (a, b, c) => a.toFixed()),
] as const)

which works, but is somewhat unfortunate.

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