1

I'm playing with the following snippet. When I remove the line validate: (...) => {}, A is correctly inferred as rule1: { v: number }. However, when the line is not removed, A is inferred as unknown. Any ideas what causes this and how to fix it? Thanks in advance :)

type Arguments<A> = {
  [K in keyof A]: {
        args: {
          [K1 in keyof A[K]]: {
              assert?: (arg: unknown) => arg is A[K][K1];
          }
        };
        validate?: (value: unknown, args: A[K]) => unknown;
  }
}

function test<A>(args: Arguments<A>) {}

test({
    rule1: {
        args: {
            v: {
                assert: (arg): arg is number => typeof arg === 'number',
            }
        },
        validate: (value, args) => {

        }
    }
});
11
  • I really confuse, can you give me any insight what do you wanna achieve here. What is a purpose here? Commented Mar 22, 2020 at 15:35
  • Test is a function that accepts a config object to produce some sort of validator (in this case the validator name is rule1). This validator may need an argument v which has to be a number. It will be checked and passed to the validate method. I want the method to be able to access the types the arguments. @MaciejSikora Commented Mar 22, 2020 at 15:40
  • This is a design limitation of TS, I think... see #12621 or #26418. Would you be open to a workaround that uses currying or multiple function arguments? Commented Mar 23, 2020 at 1:57
  • @jcalz Sure why not? Commented Mar 23, 2020 at 2:10
  • Multiple arguments: here Commented Mar 23, 2020 at 2:12

1 Answer 1

1

The problem here is that the compiler needs to both infer the generic type parameter and the type of the unannotated callback parameter, inside the same object, and it can't really do that consistently. This is a known design limitation; see microsoft/TypeScript#12621 and microsoft/TypeScript#26418 among others. The short answer here is you either have to give up on one of these inferences (by either manually specifying the generic type parameter or by manually annotating the callback parameters), or you have to break the single object apart into multiple objects so that the compiler can do the multiple inference passes in stages.

The workaround I'll present here is a helper function which builds your single object from two separate ones. The first object will be used to infer the generic type parameter, which will then be used in the contextual type of the callback parameter in the second object. (I called this "currying" in the comments but it really isn't currying, sorry about the terminology error)

const arg = function <T>(
    args: { [K in keyof T]: {
        assert?: (arg: unknown) => arg is T[K];
    } },
    validate?: (value: unknown, args: T) => unknown
) {
    return ({ args, validate });
}

The arg() function takes two parameters, args and validate, and combines them into a single object. When you call test(), now, you use arg() to build each property:

test({
    rule1: arg({
        v: {
            assert: (arg): arg is number => typeof arg === 'number',
        }
    }, (value, args) => { }
    )
});

Here the type parameter in arg() is inferred to be {v: number}, and thus the type parameter in test() is now properly inferred to be {rule1: {v: number}} as desired. It's not perfect, but it's the best I can think of for now. Okay, hope that helps; good luck!

Playground link to code

Sign up to request clarification or add additional context in comments.

1 Comment

Very useful and well explained. Would be nice if you could answer my last question in the comment above :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.