Expected 3 type arguments but got 1 but it should

2020-01-24 09:43发布

问题:

I wondering how to correctly infer 2th and 3th template of my function

suppose a simple interface

interface ISome {
    a: string;
    b?: {
        c: string;
    };
}

follow works

function pathBuilder<
    K1 extends keyof ISome,
    K2 extends keyof NonNullable<ISome[K1]>>(p: K1, p2?: K2) {
    let res = String(p);
    if (p2) { res += "." + p2; }
    return res;
}

const pathTest = pathBuilder("b", "c"); // ---> "b.c" and intellisense works on parameters

but I need to generalize the function to work by specify another type ( I don't want to pass an object instance to specify the type )

so, following not works

function pathBuilder<
    T,
    K1 extends keyof T,
    K2 extends keyof NonNullable<T[K1]>>(p: K1, p2?: K2) {
    let res = String(p);
    if (p2) { res += "." + p2; }
    return res;
}

const pathTest = pathBuilder<ISome>("b", "c"); // ERROR: Expected 3 type arguments, but got 1.ts(2558)

seems that 2th and 3th template argument of the function doesn't infer from the first one but it should because in the case first case when I specified directly a type T=ISome it worked.

I'm not sure if there is some language keyword to make it work but the template should work exactly for that: specify an unknown type.

EDIT

Actually I found this way, but require extra coding I would avoid if possible

function pathBuilder<T>() {
    return <
        K1 extends keyof T,
        K2 extends keyof NonNullable<T[K1]>>(p: K1, p2?: K2) => {
        let res = String(p);
        if (p2) { res += "." + p2; }
        return res;
    };
}

const pathTest = pathBuilder<ISome>()("b", "c");

回答1:

As of TS3.4 there is no partial type parameter inference. Either you let the compiler try to infer all the type parameters, or you specify all the type parameters. (Well, there are default type parameters but that doesn't give you what you want: you want to infer the type parameters you leave out, not assign a default type to them). There have been several proposals to address this, but so far none have met with full approval.

For now, therefore, there are only workarounds. The two that I can think of are to use a dummy function parameter or to use currying.

The dummy parameter version:

function pathBuilderDummy<
    T,
    K1 extends keyof T,
    K2 extends keyof NonNullable<T[K1]>>(dummy: T, p: K1, p2?: K2) {
    let res = String(p);
    if (p2) { res += "." + p2; }
    return res;
}

const pathDummyTest = pathBuilderDummy(null! as ISome, "b", "c");

Here we are doing what you said you didn't want to do... pass in a parameter of type T. But since it's just a dummy parameter and not used at runtime, it only matters what the type system thinks it is. The actual type of the value you pass in doesn't matter. So you can just pass it null and use a type assertion to choose T.

The curried function solution:

const pathBuilderCurry =
    <T>() => <
        K1 extends keyof T,
        K2 extends keyof NonNullable<T[K1]>>(p: K1, p2?: K2) => {
        let res = String(p);
        if (p2) { res += "." + p2; }
        return res;
    }

const pathCurryTest = pathBuilderCurry<ISome>()("b", "c")

Here you are returning a function that returns another function. The first function takes no value parameters but it does take the one type parameter you want to specify. It then returns a function where T is specified but the other type parameters are inferred.

Neither solution is perfect, but they are the best we can do for now. Hope that helps; good luck!