Search code examples
typescripttypestypeerror

TypeScript "could be instantiated with a different subtype of constraint" in case of function parameters and generics


I can't wrap my head around this error.

What I'm trying to do: make a React Hook (shown as just a function here for simplicity) that takes another function as argument. That argument-function can only accept as own argument an object that has some specific properties (ex. page and pageSize for paginated API calls - could be more (a subtype), can't be less).

Here is some explicative code:

interface MyParams {
    page: number;
    pageSize: number;
}
interface MyResponse {
    count: number;
    results: any[];
}

type Options<T extends MyParams, K extends MyResponse> = {
    getDataFn: (params: T) => Promise<K>;
    setData: (data: K) => void;
};

const elaborate = <T extends MyParams, K extends MyResponse>(
    options: Options<T, K>
) => {
    return options
        .getDataFn({ page: 0, pageSize: 100 }) // Error. Why?!
        .then((res) => options.setData(res));
};

elaborate<{}, MyResponse>({ // Error. Expected!
    getDataFn: (params) => Promise.resolve({ count: "0", results: [] }), // No error. Why?!
    setData: () => {},
});

TS PlayGround link: https://www.typescriptlang.org/play?ssl=27&ssc=1&pln=1&pc=1#code/JYOwLgpgTgZghgYwgAgLIE8AKcpwLYDOyA3gFACQADnAOYQBcyIArngEbQDcF1dAysABeDJqw5RuAX1KhIsRCgwAlCAUoB7EARRlyCdc3CMW7LhSirmAGzAFGcEOgDaAXSmlSYdJRQB5SmDAmgQAPAAqyBAAHpAgACZEGNi4hAA0yADSkTEQ8YnoKmrBEAB8yAC8JBR0YAAicGBwAGIgjAAU1Cl2yGEAlBVlmFDqeMDaIRkl3OTadQ1w7XHzjBn95WUAburAcVLcpPpaYJFWcGzquJAVyOHZsQloWDj4BOlZ0ff5hRpapW0U6gCQS0jH8gWC4TeJVIazKugsYGYUBAyEB4K0FHIADoavVGi02sRkLwRAAGdIkgTCRgARlJpOQkl6mKxYAAFrk2m0LARYaigcEsbM8XBuaper09h4IKdzpcICFiJJ0spVD9tCVCdUIHN8a1kB1noQ+UMRmMIFieeorBsIITkPpDGBGAAiUku9I86y2RiuRm9VIUYXLA18pWBpncIA

I get an error on line 19 (.getDataFn({ page: 1, pageSize: 10 })), that says: "Argument of type '{ page: number; pageSize: number; }' is not assignable to parameter of type 'T'. '{ page: number; pageSize: number; }' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'MyParams'."

So it seems that the generic T could somehow NOT CONTAIN the page and pageSize properties.

Well, that is not true, because I also get an error on line 23, where I purposely tried to make such a type mistake. The error says: "Type '{}' does not satisfy the constraint 'MyParams'. Type '{}' is missing the following properties from type 'MyParams': page, pageSize".

So I can't actually call the function elaborate and pass a generic T that does not satisfy the constraint, otherwise I get an error (as expected).

So, what is causing the error on line 19?

Also, I would expect an error on line 24 (Promise.resolve({ count: "1", results: [] })), where I purposely set count as "1" (string) instead of number, which doesn't satisfy the constraint setData: (data: K) => void; where K extends MyResponse.

Thanks to all who can shed some light on this...

EDIT - MORE CONTEXT:

I want that T may contain some other properties.

Ideally that main-function should take a dataGetter and handle its pagination automatically (code excluded). Other properties may be some filters, for example a query: string (that I handle).

It should be reusable for all paginated API, so it may have more or different subtypes, but page and pageSize are common to all.

Better code example:

interface MyParams {
    page: number;
    pageSize: number;
}

interface MyResponse {
    count: number;
    results: any[];
}

type Options<T extends MyParams, K extends MyResponse> = {
    getDataFn: (params: T) => Promise<K>;
    setData: (data: K) => void;
};

const elaborate = <T extends MyParams, K extends MyResponse>(
    options: Options<T, K>,
    otherParams: Omit<T, 'page' | 'pageSize'>
) => {
    return options
        .getDataFn({ page: 0, pageSize: 100, ...otherParams })
        .then((res) => options.setData(res));
};

///////////////////////////

type MyAPIParams = {
    page: number;
    pageSize: number;
    query: string;
}

type MyAPIResponse = {
    count: number;
    results: {name: string, age: number}[];
    otherProperty: boolean;
}

const API = {
    GET_DATA: (params: MyAPIParams): Promise<MyAPIResponse> => Promise.resolve({ count: 0, results: [], otherProperty: true})
}

elaborate<MyAPIParams, MyAPIResponse>({
    getDataFn: API.GET_DATA,
    setData: (data) => { console.log(data.results, data.otherProperty) },
}, {query: 'test'});

Playground: https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgLIE8AKcpwLYDOyA3gFACQADnAOYQBcyIArngEbQDcF1dAysABeDJqw5RuAX1KlQkWIhQYAShAKUA9iAIoy5BBubhGLdlwpQ1zADZgCjOCHQBtALpSZYdJRQB5SmDAWgQAPAAqyBAAHpAgACZEGNi4hAA0yADSkTEQ8YnoqurBEAB8yAC8JBR0YAAicGBwAGIgjAAU1Cn2yGEAlBVlmFAaeMA6IRkl3OQ6dQ1w7XHzjBn95WUAbhrAcVLcpAbaYJHWcGwauJAVyOHZsQloWDj4BOlZ0ff5hZrapW0UGgCQW0jH8gWC4TeJVSpGQcOQGjAAAtoMkXqDRmBIcgAOS8CA45AAH1x+IEwhxJVIazKeksYGYUBACKBwQo5AAdDV6o0Wm1iMh8YwAAzpMlCEQARmFouQHPliJRUDRhGQkl67I5yNybTalgINJZ4O0HNmPLgerUvV6exkAHoHY6nc6XS7PN4lOgAIKYACSKqIlTI8MFtBEpnE3BD4uEJjE5hDAEdmNB0IwCGAoKAaB5SF4fI8fb7vsVrsH4QYjGA42YJLD4fqbHZGMQQPgRBmsyAaOkwzXxJI3FH4YrUcMfFAvIxzhprBBHLnDhnkEWy-W4QBxACiYQA+rUvWEve1OujC36A71GEMRmMICEMEWS78yutkDfRjoOfrZxsIPzkErYxkFlRtbG6Nx0lHZVx2gKdkEzFN1VIaRSAgU5zkue9HwvZ40nPYs1B+HQSn5aoIDmXlWhXP0OW3PcDyPGEZgo81FnmQ0BSXWcIA5awNBoNolkab8rHA9JhLgDloJvCcvH6SQYUUkhk1TRgcUgDMcXVTggA


Solution

  • You get the error because you say that T extends MyParams, which doesn't mean that T will be strictly equal to MyParams. T may contain some other properties; thus, when you try to pass MyParams to the getDataFn you get the error.

    To solve it, you either can use the assertion:

    getDataFn({ page: 0, pageSize: 100 } as T)
    

    Another solution would be to have a generic parameter for the whole options. It works because T is definitely Options<MyParams, MyResponse> and MyParams and MyResponse can't be extended, however T may contain some extra properties, which doesn't bother us:

    <T extends Options<MyParams, MyResponse>>
    

    The last thing that you could do is remove generic and just expect Options<MyParams, MyResponse>:

    const elaborate = (options:Options<MyParams, MyResponse>) => {}
    

    The reason why you don't get the error with Promise.resolve({ count: "1", results: [] }), is simply that you purposely passed {} as first parameter, which doesn't allow the compiler to infer the types correctly. If you pass a expected type instead of {} you will see the error.

    playground for generic for the whole Options

    UPDATE

    Since you expect additional properties, we must adjust the types further. In the updated code, even though it seems correct, you get the same error, T extends MyParams may have page or other property not as any number, but a specific one like page: 1; thus, when you try to pass page: 0, typescript will complain about this.

    The easiest way to fix it would be to remove MyParams from T and assign it back.

    type RemoveMyParams<T extends MyParams> = Omit<T, keyof MyParams> & MyParams;
    

    It works because T extends MyParams, not equal to MyParams and may have more narrowed values for the properties; therefore, we remove the keys of MyParams from T and add general MyParams to it, which will have properties as any number.

    The difference between extends and & here is that extends is not strictly equal to MyParams, and adding MyParams with & is not generic, that's why T will have general MyParams, not a specific one.

    type Options<T extends MyParams, K extends MyResponse> = {
      getDataFn: (params: RemoveMyParams<T>) => Promise<K>;
      setData: (data: K) => void;
    };
    

    playground